Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-26 09:09:24 +00:00
parent 2d40635435
commit 26bba9525d
43 changed files with 889 additions and 121 deletions

View File

@ -1,20 +1,10 @@
import Vue from 'vue';
import createFlash from '~/flash';
import { parseRailsFormFields } from '~/lib/utils/forms';
import { __ } from '~/locale';
import ExpiresAtField from './components/expires_at_field.vue';
const getInputAttrs = (el) => {
const input = el.querySelector('input');
return {
id: input.id,
name: input.name,
value: input.value,
placeholder: input.placeholder,
};
};
export const initExpiresAtField = () => {
const el = document.querySelector('.js-access-tokens-expires-at');
@ -22,7 +12,7 @@ export const initExpiresAtField = () => {
return null;
}
const inputAttrs = getInputAttrs(el);
const { expiresAt: inputAttrs } = parseRailsFormFields(el);
return new Vue({
el,
@ -43,7 +33,7 @@ export const initProjectsField = () => {
return null;
}
const inputAttrs = getInputAttrs(el);
const { projects: inputAttrs } = parseRailsFormFields(el);
if (window.gon.features.personalAccessTokensScopedToProjects) {
return new Promise((resolve) => {

View File

@ -328,6 +328,7 @@ export default {
<div
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
data-testid="issue-count-badge"
:class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader,
'gl-p-0': list.collapsed,

View File

@ -2,8 +2,8 @@
import { GlButton } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
@ -17,8 +17,8 @@ export default {
ProjectSelect,
GlButton,
},
mixins: [glFeatureFlagMixin()],
inject: ['groupId', 'weightFeatureAvailable', 'boardWeight'],
mixins: [BoardNewIssueMixin],
inject: ['groupId'],
props: {
list: {
type: Object,
@ -53,14 +53,11 @@ export default {
submit(e) {
e.preventDefault();
const { title } = this;
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
const weight = this.weightFeatureAvailable ? this.boardWeight : undefined;
const { title } = this;
eventHub.$emit(`scroll-board-list-${this.list.id}`);
return this.addListNewIssue({
@ -70,7 +67,7 @@ export default {
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.selectedProject.fullPath,
weight: weight >= 0 ? weight : null,
...this.extraIssueInput(),
},
list: this.list,
}).then(() => {

View File

@ -0,0 +1,6 @@
export default {
// EE-only
methods: {
extraIssueInput: () => {},
},
};

View File

@ -327,8 +327,8 @@ export default {
commit(types.RESET_ISSUES);
},
moveItem: ({ dispatch }) => {
dispatch('moveIssue');
moveItem: ({ dispatch }, payload) => {
dispatch('moveIssue', payload);
},
moveIssue: (

View File

@ -0,0 +1,56 @@
import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg';
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { __ } from '~/locale';
const ERROR_FETCHING_DATA_HEADER = __('Could not get the data properly');
const ERROR_FETCHING_DATA_DESCRIPTION = __(
'Please try and refresh the page. If the problem persists please contact support.',
);
/**
* This function takes a Component and extends it with data from the `parseData` function.
* The data will be made available through `props` and `proivde`.
* If the `parseData` throws, the `GlEmptyState` will be returned.
* @param {Component} Component a component to render
* @param {Object} options
* @param {Function} options.parseData a function to parse `data`
* @param {Object} options.data an object to pass to `parseData`
* @param {Boolean} options.shouldLog to tell whether to log any thrown error by `parseData` to Sentry
* @param {Object} options.props to override passed `props` data
* @param {Object} options.provide to override passed `provide` data
* @param {*} ...options the remaining options will be passed as properties to `createElement`
* @return {Component} a Vue component to render, either the GlEmptyState or the extended Component
*/
export default function ensureData(Component, options = {}) {
const { parseData, data, shouldLog = false, props, provide, ...rest } = options;
try {
const parsedData = parseData(data);
return {
provide: { ...parsedData, ...provide },
render(createElement) {
return createElement(Component, {
props: { ...parsedData, ...props },
...rest,
});
},
};
} catch (error) {
if (shouldLog) {
Sentry.captureException(error);
}
return {
functional: true,
render(createElement) {
return createElement(GlEmptyState, {
props: {
title: ERROR_FETCHING_DATA_HEADER,
description: ERROR_FETCHING_DATA_DESCRIPTION,
svgPath: `data:image/svg+xml;utf8,${encodeURIComponent(emptySvg)}`,
},
});
},
};
}
}

View File

@ -1,3 +1,5 @@
import { convertToCamelCase } from '~/lib/utils/text_utility';
export const serializeFormEntries = (entries) =>
entries.reduce((acc, { name, value }) => Object.assign(acc, { [name]: value }), {});
@ -51,3 +53,95 @@ export const serializeFormObject = (form) =>
return acc;
}, []),
);
/**
* Parse inputs of HTML forms generated by Rails.
*
* This can be helpful when mounting Vue components within Rails forms.
*
* If called with an HTML element like:
*
* ```html
* <input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
* <input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contactInfoPhone">
* <input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests">
* <input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests">
* ```
*
* It will return an object like:
*
* ```javascript
* {
* contactInfoEmail: {
* name: 'user[contact_info][email]',
* id: 'user_contact_info_email',
* value: 'foo@bar.com',
* placeholder: 'Email',
* },
* contactInfoPhone: {
* name: 'user[contact_info][phone]',
* id: 'user_contact_info_phone',
* value: '(123) 456-7890',
* placeholder: 'Phone',
* },
* interests: [
* {
* name: 'user[interests][]',
* id: 'user_interests_vue',
* value: 'Vue',
* checked: true,
* },
* {
* name: 'user[interests][]',
* id: 'user_interests_graphql',
* value: 'GraphQL',
* checked: false,
* },
* ],
* }
* ```
*
* @param {HTMLInputElement} mountEl
* @returns {Object} object with form fields data.
*/
export const parseRailsFormFields = (mountEl) => {
if (!mountEl) {
throw new TypeError('`mountEl` argument is required');
}
const inputs = mountEl.querySelectorAll('[name]');
return [...inputs].reduce((accumulator, input) => {
const fieldName = input.dataset.jsName;
if (!fieldName) {
return accumulator;
}
const fieldNameCamelCase = convertToCamelCase(fieldName);
const { id, placeholder, name, value, type, checked } = input;
const attributes = {
name,
id,
value,
...(placeholder && { placeholder }),
};
// Store radio buttons and checkboxes as an array so they can be
// looped through and rendered in Vue
if (['radio', 'checkbox'].includes(type)) {
return {
...accumulator,
[fieldNameCamelCase]: [
...(accumulator[fieldNameCamelCase] || []),
{ ...attributes, checked },
],
};
}
return {
...accumulator,
[fieldNameCamelCase]: attributes,
};
}, {});
};

View File

@ -10,6 +10,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:add_issues_button)
push_frontend_feature_flag(:swimlanes_buffered_rendering, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_board_lists, project, default_enabled: :yaml)
end
feature_category :boards

View File

@ -23,7 +23,7 @@
= render_if_exists 'personal_access_tokens/callout_max_personal_access_token_lifetime'
.js-access-tokens-expires-at
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off'
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
.form-group
= f.label :scopes, _('Scopes'), class: 'label-bold'
@ -31,7 +31,7 @@
- if prefix == :personal_access_token && Feature.enabled?(:personal_access_tokens_scoped_to_projects, current_user)
.js-access-tokens-projects
%input{ type: 'hidden', name: 'temporary-name', id: 'temporary-id' }
%input{ type: 'hidden', name: 'personal_access_token[projects]', id: 'personal_access_token_projects', data: { js_name: 'projects' } }
.gl-mt-3
= f.submit _('Create %{type}') % { type: type }, class: 'gl-button btn btn-confirm', data: { qa_selector: 'create_token_button' }

View File

@ -11,6 +11,8 @@ module WorkerAttributes
# Urgencies that workers can declare through the `urgencies` attribute
VALID_URGENCIES = [:high, :low, :throttled].freeze
VALID_DATA_CONSISTENCIES = [:always, :sticky, :delayed].freeze
NAMESPACE_WEIGHTS = {
auto_devops: 2,
auto_merge: 3,
@ -69,6 +71,35 @@ module WorkerAttributes
class_attributes[:urgency] || :low
end
def data_consistency(data_consistency, feature_flag: nil)
raise ArgumentError, "Invalid data consistency: #{data_consistency}" unless VALID_DATA_CONSISTENCIES.include?(data_consistency)
raise ArgumentError, 'Data consistency is already set' if class_attributes[:data_consistency]
class_attributes[:data_consistency_feature_flag] = feature_flag if feature_flag
class_attributes[:data_consistency] = data_consistency
validate_worker_attributes!
end
def validate_worker_attributes!
# Since the deduplication should always take into account the latest binary replication pointer into account,
# not the first one, the deduplication will not work with sticky or delayed.
# Follow up issue to improve this: https://gitlab.com/gitlab-org/gitlab/-/issues/325291
if idempotent? && get_data_consistency != :always
raise ArgumentError, "Class can't be marked as idempotent if data_consistency is not set to :always"
end
end
def get_data_consistency
class_attributes[:data_consistency] || :always
end
def get_data_consistency_feature_flag_enabled?
return true unless class_attributes[:data_consistency_feature_flag]
Feature.enabled?(class_attributes[:data_consistency_feature_flag], default_enabled: :yaml)
end
# Set this attribute on a job when it will call to services outside of the
# application, such as 3rd party applications, other k8s clusters etc See
# doc/development/sidekiq_style_guide.md#jobs-with-external-dependencies for
@ -96,6 +127,8 @@ module WorkerAttributes
def idempotent!
class_attributes[:idempotent] = true
validate_worker_attributes!
end
def idempotent?

View File

@ -0,0 +1,5 @@
---
title: Fix usage data count start/finish export issue
merge_request: 57403
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Removed migrate_delayed_project_removal feature flag
merge_request: 57541
author:
type: other

View File

@ -1,8 +0,0 @@
---
name: migrate_delayed_project_removal
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53916
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300207
milestone: '13.9'
type: development
group: group::access
default_enabled: true

View File

@ -9914,7 +9914,7 @@ Counts of MAU adding epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314215451_g_project_management_users_creating_epic_notes_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9926,7 +9926,7 @@ Counts of WAU adding epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314231518_g_project_management_users_creating_epic_notes_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9938,7 +9938,7 @@ Counts of MAU destroying epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315034808_g_project_management_users_destroying_epic_notes_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9950,7 +9950,7 @@ Counts of WAU destroying epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315034846_g_project_management_users_destroying_epic_notes_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9962,7 +9962,7 @@ Counts of MAU setting epic start date as fixed
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055624_g_project_management_users_setting_epic_start_date_as_fixed_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9974,7 +9974,7 @@ Counts of WAU setting epic start date as fixed
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315054905_g_project_management_users_setting_epic_start_date_as_fixed_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9986,7 +9986,7 @@ Counts of MAU setting epic start date as inherited
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210315055439_g_project_management_users_setting_epic_start_date_as_inherited_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -9998,7 +9998,7 @@ Counts of WAU setting epic start date as inherited
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210315055342_g_project_management_users_setting_epic_start_date_as_inherited_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10010,7 +10010,7 @@ Counts of MAU changing epic descriptions
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312102051_g_project_management_users_updating_epic_descriptions_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10022,7 +10022,7 @@ Counts of WAU changing epic descriptions
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101753_g_project_management_users_updating_epic_descriptions_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10034,7 +10034,7 @@ Counts of MAU updating epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210314234202_g_project_management_users_updating_epic_notes_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10046,7 +10046,7 @@ Counts of WAU updating epic notes
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210314234041_g_project_management_users_updating_epic_notes_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10058,7 +10058,7 @@ Counts of MAU changing epic titles
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210312101935_g_project_management_users_updating_epic_titles_monthly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`
@ -10070,7 +10070,7 @@ Counts of WAU changing epic titles
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210312101826_g_project_management_users_updating_epic_titles_weekly.yml)
Group: `group:product planning`
Group: `group::product planning`
Status: `implemented`

View File

@ -16,6 +16,7 @@ module Gitlab
:elasticsearch_calls,
:elasticsearch_duration_s,
:elasticsearch_timed_out_count,
:worker_data_consistency,
*::Gitlab::Memory::Instrumentation::KEY_MAPPING.values,
*::Gitlab::Instrumentation::Redis.known_payload_keys,
*::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS,

View File

@ -43,3 +43,5 @@ module Gitlab
end
end
end
Gitlab::SidekiqMiddleware.singleton_class.prepend_if_ee('EE::Gitlab::SidekiqMiddleware')

View File

@ -5,11 +5,11 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41091
class UsageDataQueries < UsageData
class << self
def count(relation, column = nil, *rest)
def count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column)
end
def distinct_count(relation, column = nil, *rest)
def distinct_count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column, :distinct)
end
@ -21,14 +21,14 @@ module Gitlab
end
end
def sum(relation, column, *rest)
def sum(relation, column, *args, **kwargs)
relation.select(relation.all.table[column].sum).to_sql
end
# For estimated distinct count use exact query instead of hll
# buckets query, because it can't be used to obtain estimations without
# supplementary ruby code present in Gitlab::Database::PostgresHll::BatchDistinctCounter
def estimate_batch_distinct_count(relation, column = nil, *rest)
def estimate_batch_distinct_count(relation, column = nil, *args, **kwargs)
raw_sql(relation, column, :distinct)
end

View File

@ -8707,6 +8707,9 @@ msgstr ""
msgid "Could not find iteration"
msgstr ""
msgid "Could not get the data properly"
msgstr ""
msgid "Could not load the user chart. Please refresh the page to try again."
msgstr ""
@ -23056,6 +23059,9 @@ msgstr ""
msgid "Please try again"
msgstr ""
msgid "Please try and refresh the page. If the problem persists please contact support."
msgstr ""
msgid "Please type %{phrase_code} to proceed or close this modal to cancel."
msgstr ""

View File

@ -68,20 +68,30 @@ module QA
end
end
def has_child_pipeline?
has_element? :child_pipeline
def has_child_pipeline?(title: nil)
title ? find_child_pipeline_by_title(title) : has_element?(:child_pipeline)
end
def has_no_child_pipeline?
has_no_element? :child_pipeline
has_no_element?(:child_pipeline)
end
def click_job(job_name)
click_element(:job_link, Project::Job::Show, text: job_name)
end
def expand_child_pipeline
within_element(:child_pipeline) do
def child_pipelines
all_elements(:child_pipeline, minimum: 1)
end
def find_child_pipeline_by_title(title)
child_pipelines.find { |pipeline| pipeline[:title].include?(title) }
end
def expand_child_pipeline(title: nil)
child_pipeline = title ? find_child_pipeline_by_title(title) : child_pipelines.first
within_element_by_index(:child_pipeline, child_pipelines.index(child_pipeline)) do
click_element(:expand_pipeline_button)
end
end

View File

@ -0,0 +1,112 @@
# frozen_string_literal: true
require 'faker'
module QA
RSpec.describe 'Verify', :runner do
describe 'Trigger matrix' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
end
end
let!(:runner) do
Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = [executor]
end
end
before do
Flow::Login.sign_in
add_ci_files
project.visit!
Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'succeeded')
end
after do
runner.remove_via_api!
project.remove_via_api!
end
it 'creates 2 trigger jobs and passes corresponding matrix variables', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1732' do
Page::Project::Pipeline::Show.perform do |parent_pipeline|
trigger_title1 = 'deploy: [ovh, monitoring]'
trigger_title2 = 'deploy: [ovh, app]'
aggregate_failures 'Creates two child pipelines' do
expect(parent_pipeline).to have_child_pipeline(title: trigger_title1)
expect(parent_pipeline).to have_child_pipeline(title: trigger_title2)
end
# Only check output of one of the child pipelines, should be sufficient
parent_pipeline.expand_child_pipeline(title: trigger_title1)
parent_pipeline.click_job('test_vars')
end
Page::Project::Job::Show.perform do |show|
Support::Waiter.wait_until { show.successful? }
aggregate_failures 'Job output has the correct variables' do
expect(show.output).to have_content('ovh')
expect(show.output).to have_content('monitoring')
end
end
end
private
def add_ci_files
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add parent and child pipelines CI files.'
commit.add_files(
[
child_ci_file,
parent_ci_file
]
)
end
end
def parent_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
test:
stage: test
script: echo test
tags: [#{executor}]
deploy:
stage: deploy
trigger:
include: child.yml
parallel:
matrix:
- PROVIDER: ovh
STACK: [monitoring, app]
YAML
}
end
def child_ci_file
{
file_path: 'child.yml',
content: <<~YAML
test_vars:
script:
- echo $PROVIDER
- echo $STACK
tags: [#{executor}]
YAML
}
end
end
end
end

View File

@ -70,10 +70,10 @@ gitlab:
resources:
requests:
cpu: 746m
memory: 1873M
memory: 2809M
limits:
cpu: 1119m
memory: 2809M
memory: 4214M
deployment:
readinessProbe:
initialDelaySeconds: 5 # Default is 0
@ -83,10 +83,10 @@ gitlab:
resources:
requests:
cpu: 400m
memory: 50M
memory: 75M
limits:
cpu: 600m
memory: 75M
memory: 113M
readinessProbe:
initialDelaySeconds: 5 # Default is 0
periodSeconds: 15 # Default is 10

View File

@ -14,6 +14,7 @@ RSpec.describe 'Issue Boards add issue modal', :js do
let!(:issue2) { create(:issue, project: project, title: 'hij', description: 'klm') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'Issue Boards', :js do
RSpec.describe 'Project issue boards', :js do
include DragTo
include MobileHelpers
@ -23,7 +23,7 @@ RSpec.describe 'Issue Boards', :js do
context 'no lists' do
before do
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
end
it 'creates default lists' do
@ -52,6 +52,7 @@ RSpec.describe 'Issue Boards', :js do
let_it_be(:a_plus) { create(:label, project: project, name: 'A+') }
let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) }
let_it_be(:list2) { create(:list, board: board, label: development, position: 1) }
let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) }
@ -68,7 +69,7 @@ RSpec.describe 'Issue Boards', :js do
before do
stub_feature_flags(board_new_list: false)
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
wait_for_requests
@ -121,7 +122,8 @@ RSpec.describe 'Issue Boards', :js do
context 'with the NOT queries feature flag disabled' do
before do
stub_feature_flags(not_issuable_queries: false)
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
end
it 'does not have the != option' do
@ -141,7 +143,8 @@ RSpec.describe 'Issue Boards', :js do
context 'with the NOT queries feature flag enabled' do
before do
stub_feature_flags(not_issuable_queries: true)
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
end
it 'does not have the != option' do
@ -171,8 +174,7 @@ RSpec.describe 'Issue Boards', :js do
it 'infinite scrolls list' do
create_list(:labeled_issue, 50, project: project, labels: [planning])
visit project_board_path(project, board)
wait_for_requests
visit_project_board_path_without_query_limit(project, board)
page.within(find('.board:nth-child(2)')) do
expect(page.find('.board-header')).to have_content('58')
@ -180,15 +182,19 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_content('Showing 20 of 58 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 58 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
wait_for_requests
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
expect(page).to have_selector('.board-card', count: 58)
expect(page).to have_content('Showing all issues')
@ -236,13 +242,13 @@ RSpec.describe 'Issue Boards', :js do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
expect(find('.board:nth-child(3)')).to have_content(planning.title)
# Make sure list positions are preserved after a reload
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
expect(find('.board:nth-child(2)')).to have_content(development.title)
expect(find('.board:nth-child(2)')).to have_content(planning.title)
expect(find('.board:nth-child(3)')).to have_content(planning.title)
end
it 'dragging does not duplicate list' do
@ -254,7 +260,8 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_selector(selector, text: development.title, count: 1)
end
it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323551
xit 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do
drag(list_from_index: 1, from_index: 1, list_to_index: 2)
wait_for_board_cards(2, 7)
@ -467,14 +474,16 @@ RSpec.describe 'Issue Boards', :js do
end
it 'removes filtered labels' do
set_filter("label", testing.title)
click_filter_link(testing.title)
submit_filter
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
set_filter("label", testing.title)
click_filter_link(testing.title)
submit_filter
wait_for_board_cards(2, 1)
wait_for_board_cards(2, 1)
find('.clear-search').click
submit_filter
find('.clear-search').click
submit_filter
end
wait_for_board_cards(2, 8)
end
@ -484,7 +493,9 @@ RSpec.describe 'Issue Boards', :js do
set_filter("label", testing.title)
click_filter_link(testing.title)
submit_filter
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
submit_filter
end
wait_for_requests
@ -494,13 +505,18 @@ RSpec.describe 'Issue Boards', :js do
expect(page).to have_content('Showing 20 of 51 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
expect(page).to have_selector('.board-card', count: 40)
expect(page).to have_content('Showing 40 of 51 issues')
find('.board .board-list')
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight")
end
expect(page).to have_selector('.board-card', count: 51)
expect(page).to have_content('Showing all issues')
@ -569,7 +585,7 @@ RSpec.describe 'Issue Boards', :js do
context 'keyboard shortcuts' do
before do
visit project_board_path(project, board)
visit_project_board_path_without_query_limit(project, board)
wait_for_requests
end
@ -617,15 +633,19 @@ RSpec.describe 'Issue Boards', :js do
def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true)
# ensure there is enough horizontal space for four boards
resize_window(2000, 800)
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
resize_window(2000, 800)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
list_to_index: list_to_index,
perform_drop: perform_drop)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
list_to_index: list_to_index,
perform_drop: perform_drop)
end
wait_for_requests
end
def wait_for_board_cards(board_number, expected_cards)
@ -666,4 +686,10 @@ RSpec.describe 'Issue Boards', :js do
accept_confirm { find('[data-testid="remove-list"]').click }
end
end
def visit_project_board_path_without_query_limit(project, board)
inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do
visit project_board_path(project, board)
end
end
end

View File

@ -12,6 +12,8 @@ RSpec.describe 'Issue Boards add issue modal filtering', :js do
let!(:issue1) { create(:issue, project: project) }
before do
stub_feature_flags(graphql_board_lists: false)
stub_feature_flags(add_issues_button: true)
project.add_maintainer(user)
sign_in(user)

View File

@ -41,6 +41,10 @@ RSpec.describe 'Multi Select Issue', :js do
before do
project.add_maintainer(user)
# multi-drag disabled with feature flag for now
# https://gitlab.com/gitlab-org/gitlab/-/issues/289797
stub_feature_flags(graphql_board_lists: false)
sign_in(user)
end

View File

@ -3,10 +3,12 @@
require 'spec_helper'
RSpec.describe 'Issue Boards new issue', :js do
let(:project) { create(:project, :public) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, position: 0) }
let(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:board) { create(:board, project: project) }
let_it_be(:backlog_list) { create(:backlog_list, board: board) }
let_it_be(:label) { create(:label, project: project, name: 'Label 1') }
let_it_be(:list) { create(:list, board: board, label: label, position: 0) }
let_it_be(:user) { create(:user) }
context 'authorized user' do
before do
@ -15,6 +17,7 @@ RSpec.describe 'Issue Boards new issue', :js do
sign_in(user)
visit project_board_path(project, board)
wait_for_requests
expect(page).to have_selector('.board', count: 3)
@ -70,11 +73,12 @@ RSpec.describe 'Issue Boards new issue', :js do
issue = project.issues.find_by_title('bug')
expect(page).to have_content(issue.to_reference)
expect(page).to have_link(issue.title, href: issue_path(issue))
expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
end
end
it 'shows sidebar when creating new issue' do
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323446
xit 'shows sidebar when creating new issue' do
page.within(first('.board')) do
find('.issue-count-badge-add-button').click
end
@ -101,12 +105,16 @@ RSpec.describe 'Issue Boards new issue', :js do
wait_for_requests
page.within(first('.board')) do
find('.board-card').click
end
page.within(first('.issue-boards-sidebar')) do
find('.labels .edit-link').click
find('.labels [data-testid="edit-button"]').click
wait_for_requests
expect(page).to have_selector('.labels .dropdown-content li a')
expect(page).to have_selector('.labels-select-contents-list .dropdown-content li a')
end
end
end

View File

@ -43,7 +43,7 @@ RSpec.describe 'Ensure Boards do not show stale data on browser back', :js do
issue = project.issues.find_by_title('issue should be shown')
expect(page).to have_content(issue.to_reference)
expect(page).to have_link(issue.title, href: issue_path(issue))
expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
end
end
end

View File

@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar assignee', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -17,6 +17,8 @@ RSpec.describe 'Project issue boards sidebar due date', :js do
end
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -18,6 +18,8 @@ RSpec.describe 'Project issue boards sidebar labels', :js do
let(:card) { find('.board:nth-child(2)').first('.board-card') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar milestones', :js do
let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -13,6 +13,8 @@ RSpec.describe 'Project issue boards sidebar', :js do
let(:card) { find('.board:nth-child(1)').first('.board-card') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -16,6 +16,8 @@ RSpec.describe 'Project issue boards sidebar subscription', :js do
let(:card2) { find('.board:nth-child(1) .board-card:nth-of-type(2)') }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -15,6 +15,8 @@ RSpec.describe 'Project issue boards sidebar time tracking', :js do
let(:application_settings) { {} }
before do
stub_feature_flags(graphql_board_lists: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -21,7 +21,8 @@ RSpec.describe 'Sub-group project issue boards', :js do
wait_for_requests
end
it 'creates new label from sidebar' do
# TODO https://gitlab.com/gitlab-org/gitlab/-/issues/324290
xit 'creates new label from sidebar' do
find('.board-card').click
page.within '.labels' do

View File

@ -25,18 +25,22 @@ describe('access tokens', () => {
});
describe.each`
initFunction | mountSelector | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, expectedComponent }) => {
initFunction | mountSelector | fieldName | expectedComponent
${initExpiresAtField} | ${'js-access-tokens-expires-at'} | ${'expiresAt'} | ${ExpiresAtField}
${initProjectsField} | ${'js-access-tokens-projects'} | ${'projects'} | ${ProjectsField}
`('$initFunction', ({ initFunction, mountSelector, fieldName, expectedComponent }) => {
describe('when mount element exists', () => {
const nameAttribute = `access_tokens[${fieldName}]`;
const idAttribute = `access_tokens_${fieldName}`;
beforeEach(() => {
const mountEl = document.createElement('div');
mountEl.classList.add(mountSelector);
const input = document.createElement('input');
input.setAttribute('name', 'foo-bar');
input.setAttribute('id', 'foo-bar');
input.setAttribute('name', nameAttribute);
input.setAttribute('data-js-name', fieldName);
input.setAttribute('id', idAttribute);
input.setAttribute('placeholder', 'Foo bar');
input.setAttribute('value', '1,2');
@ -57,8 +61,8 @@ describe('access tokens', () => {
expect(component.exists()).toBe(true);
expect(component.props('inputAttrs')).toEqual({
name: 'foo-bar',
id: 'foo-bar',
name: nameAttribute,
id: idAttribute,
value: '1,2',
placeholder: 'Foo bar',
});

View File

@ -639,10 +639,13 @@ describe('resetIssues', () => {
});
describe('moveItem', () => {
it('should dispatch moveIssue action', () => {
it('should dispatch moveIssue action with payload', () => {
const payload = { mock: 'payload' };
testAction({
action: actions.moveItem,
expectedActions: [{ type: 'moveIssue' }],
payload,
expectedActions: [{ type: 'moveIssue', payload }],
});
});
});

View File

@ -1,4 +1,9 @@
import { serializeForm, serializeFormObject, isEmptyValue } from '~/lib/utils/forms';
import {
serializeForm,
serializeFormObject,
isEmptyValue,
parseRailsFormFields,
} from '~/lib/utils/forms';
describe('lib/utils/forms', () => {
const createDummyForm = (inputs) => {
@ -135,4 +140,160 @@ describe('lib/utils/forms', () => {
});
});
});
describe('parseRailsFormFields', () => {
let mountEl;
beforeEach(() => {
mountEl = document.createElement('div');
mountEl.classList.add('js-foo-bar');
});
afterEach(() => {
mountEl = null;
});
it('parses fields generated by Rails and returns object with HTML attributes', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
<input type="text" placeholder="Email" value="foo@bar.com" name="user[contact_info][email]" id="user_contact_info_email" data-js-name="contactInfoEmail">
<input type="text" placeholder="Phone" value="(123) 456-7890" name="user[contact_info][phone]" id="user_contact_info_phone" data-js-name="contact_info_phone">
<input type="hidden" placeholder="Job title" value="" name="user[job_title]" id="user_job_title" data-js-name="jobTitle">
<textarea name="user[bio]" id="user_bio" data-js-name="bio">Foo bar</textarea>
<select name="user[timezone]" id="user_timezone" data-js-name="timezone">
<option value="utc+12">[UTC - 12] International Date Line West</option>
<option value="utc+11" selected>[UTC - 11] American Samoa</option>
</select>
<input type="checkbox" name="user[interests][]" id="user_interests_vue" value="Vue" checked data-js-name="interests">
<input type="checkbox" name="user[interests][]" id="user_interests_graphql" value="GraphQL" data-js-name="interests">
<input type="radio" name="user[access_level]" value="regular" id="user_access_level_regular" data-js-name="accessLevel">
<input type="radio" name="user[access_level]" value="admin" id="user_access_level_admin" checked data-js-name="access_level">
<input name="user[private_profile]" type="hidden" value="0">
<input type="radio" name="user[private_profile]" id="user_private_profile" value="1" checked data-js-name="privateProfile">
<input name="user[email_notifications]" type="hidden" value="0">
<input type="radio" name="user[email_notifications]" id="user_email_notifications" value="1" data-js-name="emailNotifications">
`;
expect(parseRailsFormFields(mountEl)).toEqual({
name: {
name: 'user[name]',
id: 'user_name',
value: 'Administrator',
placeholder: 'Name',
},
contactInfoEmail: {
name: 'user[contact_info][email]',
id: 'user_contact_info_email',
value: 'foo@bar.com',
placeholder: 'Email',
},
contactInfoPhone: {
name: 'user[contact_info][phone]',
id: 'user_contact_info_phone',
value: '(123) 456-7890',
placeholder: 'Phone',
},
jobTitle: {
name: 'user[job_title]',
id: 'user_job_title',
value: '',
placeholder: 'Job title',
},
bio: {
name: 'user[bio]',
id: 'user_bio',
value: 'Foo bar',
},
timezone: {
name: 'user[timezone]',
id: 'user_timezone',
value: 'utc+11',
},
interests: [
{
name: 'user[interests][]',
id: 'user_interests_vue',
value: 'Vue',
checked: true,
},
{
name: 'user[interests][]',
id: 'user_interests_graphql',
value: 'GraphQL',
checked: false,
},
],
accessLevel: [
{
name: 'user[access_level]',
id: 'user_access_level_regular',
value: 'regular',
checked: false,
},
{
name: 'user[access_level]',
id: 'user_access_level_admin',
value: 'admin',
checked: true,
},
],
privateProfile: [
{
name: 'user[private_profile]',
id: 'user_private_profile',
value: '1',
checked: true,
},
],
emailNotifications: [
{
name: 'user[email_notifications]',
id: 'user_email_notifications',
value: '1',
checked: false,
},
],
});
});
it('returns an empty object if there are no inputs', () => {
expect(parseRailsFormFields(mountEl)).toEqual({});
});
it('returns an empty object if inputs do not have `name` attributes', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" id="user_name">
<input type="text" placeholder="Email" value="foo@bar.com" id="user_contact_info_email">
<input type="text" placeholder="Phone" value="(123) 456-7890" id="user_contact_info_phone">
`;
expect(parseRailsFormFields(mountEl)).toEqual({});
});
it('does not include field if `data-js-name` attribute is missing', () => {
mountEl.innerHTML = `
<input type="text" placeholder="Name" value="Administrator" name="user[name]" id="user_name" data-js-name="name">
<input type="text" placeholder="Email" value="foo@bar.com" name="user[email]" id="email">
`;
expect(parseRailsFormFields(mountEl)).toEqual({
name: {
name: 'user[name]',
id: 'user_name',
value: 'Administrator',
placeholder: 'Name',
},
});
});
it('throws error if `mountEl` argument is not passed', () => {
expect(() => parseRailsFormFields()).toThrow(new TypeError('`mountEl` argument is required'));
});
it('throws error if `mountEl` argument is `null`', () => {
expect(() => parseRailsFormFields(null)).toThrow(
new TypeError('`mountEl` argument is required'),
);
});
});
});

View File

@ -0,0 +1,145 @@
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { mount } from '@vue/test-utils';
import ensureData from '~/ensure_data';
const mockData = { message: 'Hello there' };
const defaultOptions = {
parseData: () => mockData,
data: mockData,
};
const MockChildComponent = {
inject: ['message'],
render(createElement) {
return createElement('h1', this.message);
},
};
const MockParentComponent = {
components: {
MockChildComponent,
},
props: {
message: {
type: String,
required: true,
},
otherProp: {
type: Boolean,
default: false,
required: false,
},
},
render(createElement) {
return createElement('div', [this.message, createElement(MockChildComponent)]);
},
};
describe('EnsureData', () => {
let wrapper;
function findEmptyState() {
return wrapper.findComponent(GlEmptyState);
}
function findChild() {
return wrapper.findComponent(MockChildComponent);
}
function findParent() {
return wrapper.findComponent(MockParentComponent);
}
function createComponent(options = defaultOptions) {
return mount(ensureData(MockParentComponent, options));
}
beforeEach(() => {
Sentry.captureException = jest.fn();
});
afterEach(() => {
wrapper.destroy();
Sentry.captureException.mockClear();
});
describe('when parseData throws', () => {
it('should render GlEmptyState', () => {
wrapper = createComponent({
parseData: () => {
throw new Error();
},
});
expect(findParent().exists()).toBe(false);
expect(findChild().exists()).toBe(false);
expect(findEmptyState().exists()).toBe(true);
});
it('should not log to Sentry when shouldLog=false (default)', () => {
wrapper = createComponent({
parseData: () => {
throw new Error();
},
});
expect(Sentry.captureException).not.toHaveBeenCalled();
});
it('should log to Sentry when shouldLog=true', () => {
const error = new Error('Error!');
wrapper = createComponent({
parseData: () => {
throw error;
},
shouldLog: true,
});
expect(Sentry.captureException).toHaveBeenCalledWith(error);
});
});
describe('when parseData succeeds', () => {
it('should render MockParentComponent and MockChildComponent', () => {
wrapper = createComponent();
expect(findEmptyState().exists()).toBe(false);
expect(findParent().exists()).toBe(true);
expect(findChild().exists()).toBe(true);
});
it('enables user to provide data to child components', () => {
wrapper = createComponent();
const childComponent = findChild();
expect(childComponent.text()).toBe(mockData.message);
});
it('enables user to override provide data', () => {
const message = 'Another message';
wrapper = createComponent({ ...defaultOptions, provide: { message } });
const childComponent = findChild();
expect(childComponent.text()).toBe(message);
});
it('enables user to pass props to parent component', () => {
wrapper = createComponent();
expect(findParent().props()).toMatchObject(mockData);
});
it('enables user to override props data', () => {
const props = { message: 'Another message', otherProp: true };
wrapper = createComponent({ ...defaultOptions, props });
expect(findParent().props()).toMatchObject(props);
});
it('should not log to Sentry when shouldLog=true', () => {
wrapper = createComponent({ ...defaultOptions, shouldLog: true });
expect(Sentry.captureException).not.toHaveBeenCalled();
});
});
});

View File

@ -17,6 +17,7 @@ RSpec.describe Gitlab::InstrumentationHelper do
:elasticsearch_calls,
:elasticsearch_duration_s,
:elasticsearch_timed_out_count,
:worker_data_consistency,
:mem_objects,
:mem_bytes,
:mem_mallocs,

View File

@ -11,12 +11,24 @@ RSpec.describe Gitlab::UsageDataQueries do
it 'returns the raw SQL' do
expect(described_class.count(User)).to start_with('SELECT COUNT("users"."id") FROM "users"')
end
it 'does not mix a nil column with keyword arguments' do
expect(described_class).to receive(:raw_sql).with(User, nil)
described_class.count(User, start: 1, finish: 2)
end
end
describe '.distinct_count' do
it 'returns the raw SQL' do
expect(described_class.distinct_count(Issue, :author_id)).to eq('SELECT COUNT(DISTINCT "issues"."author_id") FROM "issues"')
end
it 'does not mix a nil column with keyword arguments' do
expect(described_class).to receive(:raw_sql).with(Issue, nil, :distinct)
described_class.distinct_count(Issue, nil, start: 1, finish: 2)
end
end
describe '.redis_usage_data' do

View File

@ -2,25 +2,26 @@
module NextInstanceOf
def expect_next_instance_of(klass, *new_args, &blk)
stub_new(expect(klass), nil, *new_args, &blk)
stub_new(expect(klass), nil, false, *new_args, &blk)
end
def expect_next_instances_of(klass, number, *new_args, &blk)
stub_new(expect(klass), number, *new_args, &blk)
def expect_next_instances_of(klass, number, ordered = false, *new_args, &blk)
stub_new(expect(klass), number, ordered, *new_args, &blk)
end
def allow_next_instance_of(klass, *new_args, &blk)
stub_new(allow(klass), nil, *new_args, &blk)
stub_new(allow(klass), nil, false, *new_args, &blk)
end
def allow_next_instances_of(klass, number, *new_args, &blk)
stub_new(allow(klass), number, *new_args, &blk)
def allow_next_instances_of(klass, number, ordered = false, *new_args, &blk)
stub_new(allow(klass), number, ordered, *new_args, &blk)
end
private
def stub_new(target, number, *new_args, &blk)
def stub_new(target, number, ordered = false, *new_args, &blk)
receive_new = receive(:new)
receive_new.ordered if ordered
receive_new.exactly(number).times if number
receive_new.with(*new_args) if new_args.any?

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkerAttributes do
let(:worker) do
Class.new do
def self.name
"TestWorker"
end
include ApplicationWorker
end
end
describe '.data_consistency' do
context 'with valid data_consistency' do
it 'returns correct data_consistency' do
worker.data_consistency(:sticky)
expect(worker.get_data_consistency).to eq(:sticky)
end
end
context 'when data_consistency is not provided' do
it 'defaults to :always' do
expect(worker.get_data_consistency).to eq(:always)
end
end
context 'with invalid data_consistency' do
it 'raise exception' do
expect { worker.data_consistency(:invalid) }
.to raise_error('Invalid data consistency: invalid')
end
end
context 'when job is idempotent' do
context 'when data_consistency is not :always' do
it 'raise exception' do
worker.idempotent!
expect { worker.data_consistency(:sticky) }
.to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always")
end
end
context 'when feature_flag is provided' do
before do
stub_feature_flags(test_feature_flag: false)
skip_feature_flags_yaml_validation
skip_default_enabled_yaml_check
end
it 'returns correct feature flag value' do
worker.data_consistency(:sticky, feature_flag: :test_feature_flag)
expect(worker.get_data_consistency_feature_flag_enabled?).not_to be_truthy
end
end
end
end
describe '.idempotent!' do
context 'when data consistency is not :always' do
it 'raise exception' do
worker.data_consistency(:sticky)
expect { worker.idempotent! }
.to raise_error("Class can't be marked as idempotent if data_consistency is not set to :always")
end
end
end
end