Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8212b8fd70
commit
1c1b987177
6
Gemfile
6
Gemfile
|
|
@ -476,7 +476,7 @@ group :development, :test do
|
|||
|
||||
gem 'awesome_print', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
gem 'database_cleaner-active_record', '~> 2.1.0', feature_category: :database
|
||||
gem 'database_cleaner-active_record', '~> 2.2.0', feature_category: :database
|
||||
gem 'rspec-rails', '~> 6.1.1', feature_category: :shared
|
||||
gem 'factory_bot_rails', '~> 6.4.3', feature_category: :tooling
|
||||
|
||||
|
|
@ -515,7 +515,7 @@ group :development, :test do
|
|||
# For now we only use vite in development / test, and not for production builds
|
||||
# See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/106
|
||||
gem 'vite_rails', '~> 3.0.17', feature_category: :shared
|
||||
gem 'vite_ruby', '~> 3.5.0', feature_category: :shared
|
||||
gem 'vite_ruby', '~> 3.7.0', feature_category: :shared
|
||||
|
||||
gem 'gitlab-housekeeper', path: 'gems/gitlab-housekeeper', feature_category: :tooling
|
||||
end
|
||||
|
|
@ -569,7 +569,7 @@ group :test do
|
|||
# Moved in `test` because https://gitlab.com/gitlab-org/gitlab/-/issues/217527
|
||||
gem 'derailed_benchmarks', require: false # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.31.0', require: false, feature_category: :tooling
|
||||
gem 'gitlab_quality-test_tooling', '~> 1.32.0', require: false, feature_category: :tooling
|
||||
end
|
||||
|
||||
gem 'octokit', '~> 9.0', feature_category: :importers
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@
|
|||
{"name":"cvss-suite","version":"3.0.1","platform":"ruby","checksum":"b5ca9e9e94032a42fd0dc28c1e305378b62c949e35ed7111fc4a1d76f68ad3f9"},
|
||||
{"name":"danger","version":"9.4.2","platform":"ruby","checksum":"43e552c6731030235a30fdeafe703d2e2ab9c30917154489cb0ecd9ad3259d80"},
|
||||
{"name":"danger-gitlab","version":"8.0.0","platform":"ruby","checksum":"497dd7d0f6513913de651019223d8058cf494df10acbd17de92b175dfa04a3a8"},
|
||||
{"name":"database_cleaner-active_record","version":"2.1.0","platform":"ruby","checksum":"7384b973d67bcc1b5a850b876a4638aa83cca3bc88f9d87562fe25cd2dd60d8a"},
|
||||
{"name":"database_cleaner-active_record","version":"2.2.0","platform":"ruby","checksum":"3228d6d8ec1f2103fd6ab468dae923424318bcfabcf5dd5b02e5fcb0c486e1c7"},
|
||||
{"name":"database_cleaner-core","version":"2.0.1","platform":"ruby","checksum":"8646574c32162e59ed7b5258a97a208d3c44551b854e510994f24683865d846c"},
|
||||
{"name":"date","version":"3.3.3","platform":"java","checksum":"584e0a582d1eb2207b4eaac089d8a43f2ca10bea02682f286099642f15c56cce"},
|
||||
{"name":"date","version":"3.3.3","platform":"ruby","checksum":"819792019d5712b748fb15f6dfaaedef14b0328723ef23583ea35f186774530f"},
|
||||
|
|
@ -227,7 +227,7 @@
|
|||
{"name":"gitlab-styles","version":"12.0.1","platform":"ruby","checksum":"d8a302b0ab0e1f18e2d11501760f1b85c5e70b5e5ca628828a0786c7984ed133"},
|
||||
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
|
||||
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
|
||||
{"name":"gitlab_quality-test_tooling","version":"1.31.0","platform":"ruby","checksum":"c13d38f2ba01469179db7211008722b1f4a55270cca561d832b1eee438124f52"},
|
||||
{"name":"gitlab_quality-test_tooling","version":"1.32.0","platform":"ruby","checksum":"72b7bb243d3e1f8006bef4bcf3585480a774f72bb9dd97bfcc52db4b32fd30fb"},
|
||||
{"name":"globalid","version":"1.1.0","platform":"ruby","checksum":"b337e1746f0c8cb0a6c918234b03a1ddeb4966206ce288fbb57779f59b2d154f"},
|
||||
{"name":"gon","version":"6.4.0","platform":"ruby","checksum":"e3a618d659392890f1aa7db420f17c75fd7d35aeb5f8fe003697d02c4b88d2f0"},
|
||||
{"name":"google-apis-androidpublisher_v3","version":"0.34.0","platform":"ruby","checksum":"d7e1d7dd92f79c498fe2082222a1740d788e022e660c135564b3fd299cab5425"},
|
||||
|
|
@ -749,7 +749,7 @@
|
|||
{"name":"view_component","version":"3.13.0","platform":"ruby","checksum":"316e6479f51387160e7eb372d33cbb97d586ac2ed9d9fe80495cec48b346187b"},
|
||||
{"name":"virtus","version":"2.0.0","platform":"ruby","checksum":"8841dae4eb7fcc097320ba5ea516bf1839e5d056c61ee27138aa4bddd6e3d1c2"},
|
||||
{"name":"vite_rails","version":"3.0.17","platform":"ruby","checksum":"b90e85a3e55802981cbdb43a4101d944b1e7055bfe85599d9cb7de0f1ea58bcc"},
|
||||
{"name":"vite_ruby","version":"3.5.0","platform":"ruby","checksum":"a3e5da3fdd816f831cb1530c4001a790aac862c89f74c09f48d5a3cfed3dea73"},
|
||||
{"name":"vite_ruby","version":"3.7.0","platform":"ruby","checksum":"4a34ada95e66821390896cf518c772146befc8f23d8954fa04416a5537e5f739"},
|
||||
{"name":"vmstat","version":"2.3.0","platform":"ruby","checksum":"ab5446a3e3bd0a9cdb9d9ac69a0bbd119c4f161d945a0846a519dd7018af656d"},
|
||||
{"name":"warden","version":"1.2.9","platform":"ruby","checksum":"46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0"},
|
||||
{"name":"warning","version":"1.3.0","platform":"ruby","checksum":"23695a5d8e50bd5c46068931b529bee0b28e4982cbcefbe77d867800dde8069e"},
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -455,7 +455,7 @@ GEM
|
|||
danger-gitlab (8.0.0)
|
||||
danger
|
||||
gitlab (~> 4.2, >= 4.2.0)
|
||||
database_cleaner-active_record (2.1.0)
|
||||
database_cleaner-active_record (2.2.0)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
database_cleaner-core (2.0.1)
|
||||
|
|
@ -748,7 +748,7 @@ GEM
|
|||
omniauth (>= 1.3, < 3)
|
||||
pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
|
||||
rubyntlm (~> 0.5)
|
||||
gitlab_quality-test_tooling (1.31.0)
|
||||
gitlab_quality-test_tooling (1.32.0)
|
||||
activesupport (>= 7.0, < 7.2)
|
||||
amatch (~> 0.4.1)
|
||||
gitlab (~> 4.19)
|
||||
|
|
@ -1874,7 +1874,7 @@ GEM
|
|||
vite_rails (3.0.17)
|
||||
railties (>= 5.1, < 8)
|
||||
vite_ruby (~> 3.0, >= 3.2.2)
|
||||
vite_ruby (3.5.0)
|
||||
vite_ruby (3.7.0)
|
||||
dry-cli (>= 0.7, < 2)
|
||||
rack-proxy (~> 0.6, >= 0.6.1)
|
||||
zeitwerk (~> 2.2)
|
||||
|
|
@ -1973,7 +1973,7 @@ DEPENDENCIES
|
|||
cssbundling-rails (= 1.4.0)
|
||||
csv_builder!
|
||||
cvss-suite (~> 3.0.1)
|
||||
database_cleaner-active_record (~> 2.1.0)
|
||||
database_cleaner-active_record (~> 2.2.0)
|
||||
deckar01-task_list (= 2.3.4)
|
||||
declarative_policy (~> 1.1.0)
|
||||
deprecation_toolkit (~> 1.5.1)
|
||||
|
|
@ -2043,7 +2043,7 @@ DEPENDENCIES
|
|||
gitlab-utils!
|
||||
gitlab_chronic_duration (~> 0.12)
|
||||
gitlab_omniauth-ldap (~> 2.2.0)
|
||||
gitlab_quality-test_tooling (~> 1.31.0)
|
||||
gitlab_quality-test_tooling (~> 1.32.0)
|
||||
gon (~> 6.4.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.34.0)
|
||||
google-apis-cloudbilling_v1 (~> 0.21.0)
|
||||
|
|
@ -2273,7 +2273,7 @@ DEPENDENCIES
|
|||
version_sorter (~> 2.3)
|
||||
view_component (~> 3.13.0)
|
||||
vite_rails (~> 3.0.17)
|
||||
vite_ruby (~> 3.5.0)
|
||||
vite_ruby (~> 3.7.0)
|
||||
vmstat (~> 2.3.0)
|
||||
warning (~> 1.3.0)
|
||||
webauthn (~> 3.0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
#import "~/graphql_shared/fragments/page_info.fragment.graphql"
|
||||
|
||||
fragment Group on Group {
|
||||
id
|
||||
name
|
||||
fullName
|
||||
fullPath
|
||||
}
|
||||
|
||||
query getSubGroups($fullPath: ID!, $search: String, $after: String) {
|
||||
group(fullPath: $fullPath) {
|
||||
...Group
|
||||
descendantGroups(search: $search, after: $after, first: 100) {
|
||||
nodes {
|
||||
...Group
|
||||
}
|
||||
pageInfo {
|
||||
...PageInfo
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
TOKEN_TYPE_CONTACT,
|
||||
TOKEN_TYPE_DRAFT,
|
||||
TOKEN_TYPE_EPIC,
|
||||
TOKEN_TYPE_GROUP,
|
||||
TOKEN_TYPE_HEALTH,
|
||||
TOKEN_TYPE_ITERATION,
|
||||
TOKEN_TYPE_LABEL,
|
||||
|
|
@ -235,6 +236,16 @@ export const filtersMap = {
|
|||
},
|
||||
},
|
||||
},
|
||||
[TOKEN_TYPE_GROUP]: {
|
||||
[API_PARAM]: {
|
||||
[NORMAL_FILTER]: 'fullPath',
|
||||
},
|
||||
[URL_PARAM]: {
|
||||
[OPERATOR_IS]: {
|
||||
[NORMAL_FILTER]: 'group_path',
|
||||
},
|
||||
},
|
||||
},
|
||||
[TOKEN_TYPE_REVIEWER]: {
|
||||
[API_PARAM]: {
|
||||
[NORMAL_FILTER]: 'reviewerUsername',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
<script>
|
||||
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
|
||||
import { pick } from 'lodash';
|
||||
import { createAlert } from '~/alert';
|
||||
import searchGroupsQuery from '~/boards/graphql/sub_groups.query.graphql';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { __ } from '~/locale';
|
||||
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
|
||||
|
||||
export default {
|
||||
separator: '::',
|
||||
components: {
|
||||
BaseToken,
|
||||
GlFilteredSearchSuggestion,
|
||||
},
|
||||
props: {
|
||||
config: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
groups: this.config.initialGroups || [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
defaultGroups() {
|
||||
return this.config.defaultGroups || [];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
fetchGroups(search = '') {
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: searchGroupsQuery,
|
||||
variables: { fullPath: this.config.fullPath, search },
|
||||
})
|
||||
.then(({ data }) => data.group);
|
||||
},
|
||||
fetchGroupsBySearchTerm(search) {
|
||||
this.loading = true;
|
||||
this.fetchGroups(search)
|
||||
.then((response) => {
|
||||
const parentGroup = pick(response, ['id', 'name', 'fullName', 'fullPath']) || {};
|
||||
this.groups = [parentGroup, ...(response?.descendantGroups?.nodes || [])];
|
||||
})
|
||||
.catch(() => createAlert({ message: __('There was a problem fetching groups.') }))
|
||||
.finally(() => {
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
getActiveGroup(groups, data) {
|
||||
if (data && groups.length) {
|
||||
return groups.find((group) => this.getValue(group) === data);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
getValue(group) {
|
||||
return group.fullPath;
|
||||
},
|
||||
displayValue(group) {
|
||||
return `${this.getGroupIdProperty(group)}${this.$options.separator}${group?.fullName}`;
|
||||
},
|
||||
getGroupIdProperty(group) {
|
||||
return getIdFromGraphQLId(group.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<base-token
|
||||
:config="config"
|
||||
:value="value"
|
||||
:active="active"
|
||||
:suggestions-loading="loading"
|
||||
:suggestions="groups"
|
||||
:get-active-token-value="getActiveGroup"
|
||||
:default-suggestions="defaultGroups"
|
||||
search-by="title"
|
||||
:value-identifier="getValue"
|
||||
v-bind="$attrs"
|
||||
@fetch-suggestions="fetchGroupsBySearchTerm"
|
||||
v-on="$listeners"
|
||||
>
|
||||
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
|
||||
{{ activeTokenValue ? displayValue(activeTokenValue) : inputValue }}
|
||||
</template>
|
||||
<template #suggestions-list="{ suggestions }">
|
||||
<gl-filtered-search-suggestion
|
||||
v-for="group in suggestions"
|
||||
:key="group.id"
|
||||
:value="getValue(group)"
|
||||
>
|
||||
{{ group.fullName }}
|
||||
</gl-filtered-search-suggestion>
|
||||
</template>
|
||||
</base-token>
|
||||
</template>
|
||||
|
|
@ -12,6 +12,7 @@ import {
|
|||
WORKSPACE_GROUP,
|
||||
WORKSPACE_PROJECT,
|
||||
} from '~/issues/constants';
|
||||
import { AutocompleteCache } from '~/issues/dashboard/utils';
|
||||
import { defaultTypeTokenOptions } from '~/issues/list/constants';
|
||||
import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
|
||||
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
|
||||
|
|
@ -29,14 +30,20 @@ import {
|
|||
OPERATORS_IS_NOT_OR,
|
||||
TOKEN_TITLE_ASSIGNEE,
|
||||
TOKEN_TITLE_AUTHOR,
|
||||
TOKEN_TITLE_CONFIDENTIAL,
|
||||
TOKEN_TITLE_GROUP,
|
||||
TOKEN_TITLE_LABEL,
|
||||
TOKEN_TITLE_MILESTONE,
|
||||
TOKEN_TITLE_MY_REACTION,
|
||||
TOKEN_TITLE_SEARCH_WITHIN,
|
||||
TOKEN_TITLE_TYPE,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
TOKEN_TYPE_GROUP,
|
||||
TOKEN_TYPE_LABEL,
|
||||
TOKEN_TYPE_MILESTONE,
|
||||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
|
|
@ -47,11 +54,15 @@ import { STATE_CLOSED } from '../../constants';
|
|||
import { sortOptions, urlSortParams } from '../constants';
|
||||
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
|
||||
|
||||
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
|
||||
const EmojiToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
|
||||
const GroupToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/group_token.vue');
|
||||
const LabelToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
|
||||
const MilestoneToken = () =>
|
||||
import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
|
||||
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
|
||||
|
||||
export default {
|
||||
issuableListTabs,
|
||||
|
|
@ -63,7 +74,14 @@ export default {
|
|||
IssueCardTimeInfo,
|
||||
},
|
||||
mixins: [glFeatureFlagMixin()],
|
||||
inject: ['fullPath', 'initialSort', 'isGroup', 'isSignedIn', 'workItemType'],
|
||||
inject: [
|
||||
'autocompleteAwardEmojisPath',
|
||||
'fullPath',
|
||||
'initialSort',
|
||||
'isGroup',
|
||||
'isSignedIn',
|
||||
'workItemType',
|
||||
],
|
||||
props: {
|
||||
eeCreatedWorkItemsCount: {
|
||||
type: Number,
|
||||
|
|
@ -97,6 +115,7 @@ export default {
|
|||
search: this.searchQuery,
|
||||
...this.apiFilterParams,
|
||||
...this.pageParams,
|
||||
includeDescendants: !this.apiFilterParams.fullPath,
|
||||
types: this.apiFilterParams.types || [this.workItemType],
|
||||
};
|
||||
},
|
||||
|
|
@ -185,6 +204,15 @@ export default {
|
|||
preloadedUsers,
|
||||
multiSelect: this.glFeatures.groupMultiSelectTokens,
|
||||
},
|
||||
{
|
||||
type: TOKEN_TYPE_GROUP,
|
||||
icon: 'group',
|
||||
title: TOKEN_TITLE_GROUP,
|
||||
unique: true,
|
||||
token: GroupToken,
|
||||
operators: OPERATORS_IS,
|
||||
fullPath: this.fullPath,
|
||||
},
|
||||
{
|
||||
type: TOKEN_TYPE_LABEL,
|
||||
title: TOKEN_TITLE_LABEL,
|
||||
|
|
@ -231,6 +259,31 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.isSignedIn) {
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_CONFIDENTIAL,
|
||||
title: TOKEN_TITLE_CONFIDENTIAL,
|
||||
icon: 'eye-slash',
|
||||
token: GlFilteredSearchToken,
|
||||
unique: true,
|
||||
operators: OPERATORS_IS,
|
||||
options: [
|
||||
{ icon: 'eye-slash', value: 'yes', title: __('Yes') },
|
||||
{ icon: 'eye', value: 'no', title: __('No') },
|
||||
],
|
||||
});
|
||||
|
||||
tokens.push({
|
||||
type: TOKEN_TYPE_MY_REACTION,
|
||||
title: TOKEN_TITLE_MY_REACTION,
|
||||
icon: 'thumb-up',
|
||||
token: EmojiToken,
|
||||
unique: true,
|
||||
fetchEmojis: this.fetchEmojis,
|
||||
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`,
|
||||
});
|
||||
}
|
||||
|
||||
tokens.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
return tokens;
|
||||
|
|
@ -251,7 +304,18 @@ export default {
|
|||
this.$apollo.queries.workItems.refetch();
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.autocompleteCache = new AutocompleteCache();
|
||||
},
|
||||
methods: {
|
||||
fetchEmojis(search) {
|
||||
return this.autocompleteCache.fetch({
|
||||
url: this.autocompleteAwardEmojisPath,
|
||||
cacheName: 'emojis',
|
||||
searchProperty: 'name',
|
||||
search,
|
||||
});
|
||||
},
|
||||
fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
|
||||
return this.$apollo
|
||||
.query({
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export const mountWorkItemsListApp = () => {
|
|||
Vue.use(VueApollo);
|
||||
|
||||
const {
|
||||
autocompleteAwardEmojisPath,
|
||||
fullPath,
|
||||
hasEpicsFeature,
|
||||
hasIssuableHealthStatusFeature,
|
||||
|
|
@ -34,6 +35,7 @@ export const mountWorkItemsListApp = () => {
|
|||
name: 'WorkItemsListRoot',
|
||||
apolloProvider,
|
||||
provide: {
|
||||
autocompleteAwardEmojisPath,
|
||||
fullPath,
|
||||
hasEpicsFeature: parseBoolean(hasEpicsFeature),
|
||||
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#import "ee_else_ce/work_items/list/queries/work_item_widgets.fragment.graphql"
|
||||
|
||||
query getWorkItems(
|
||||
$includeDescendants: Boolean = true
|
||||
$fullPath: ID!
|
||||
$search: String
|
||||
$sort: WorkItemSort
|
||||
|
|
@ -9,9 +10,11 @@ query getWorkItems(
|
|||
$assigneeWildcardId: AssigneeWildcardId
|
||||
$assigneeUsernames: [String!]
|
||||
$authorUsername: String
|
||||
$confidential: Boolean
|
||||
$labelName: [String!]
|
||||
$milestoneTitle: [String!]
|
||||
$milestoneWildcardId: MilestoneWildcardId
|
||||
$myReactionEmoji: String
|
||||
$types: [IssueType!]
|
||||
$in: [IssuableSearchableField!]
|
||||
$not: NegatedWorkItemFilterInput
|
||||
|
|
@ -24,15 +27,17 @@ query getWorkItems(
|
|||
group(fullPath: $fullPath) {
|
||||
id
|
||||
workItemStateCounts(
|
||||
includeDescendants: true
|
||||
includeDescendants: $includeDescendants
|
||||
sort: $sort
|
||||
state: $state
|
||||
assigneeUsernames: $assigneeUsernames
|
||||
assigneeWildcardId: $assigneeWildcardId
|
||||
authorUsername: $authorUsername
|
||||
confidential: $confidential
|
||||
labelName: $labelName
|
||||
milestoneTitle: $milestoneTitle
|
||||
milestoneWildcardId: $milestoneWildcardId
|
||||
myReactionEmoji: $myReactionEmoji
|
||||
types: $types
|
||||
not: $not
|
||||
or: $or
|
||||
|
|
@ -49,9 +54,11 @@ query getWorkItems(
|
|||
assigneeUsernames: $assigneeUsernames
|
||||
assigneeWildcardId: $assigneeWildcardId
|
||||
authorUsername: $authorUsername
|
||||
confidential: $confidential
|
||||
labelName: $labelName
|
||||
milestoneTitle: $milestoneTitle
|
||||
milestoneWildcardId: $milestoneWildcardId
|
||||
myReactionEmoji: $myReactionEmoji
|
||||
types: $types
|
||||
in: $in
|
||||
not: $not
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ module Repositories
|
|||
|
||||
Onboarding::ProgressService.async(project.namespace_id).execute(action: :git_pull)
|
||||
|
||||
return if Feature.enabled?(:disable_git_http_fetch_writes)
|
||||
return if skip_fetch_statistics_increment?
|
||||
|
||||
Projects::FetchStatisticsIncrementService.new(project).execute
|
||||
end
|
||||
|
|
@ -140,6 +140,14 @@ module Repositories
|
|||
payload[:metadata] ||= {}
|
||||
payload[:metadata][:repository_storage] = project&.repository_storage
|
||||
end
|
||||
|
||||
def skip_fetch_statistics_increment?
|
||||
# Since disable_git_http_fetch_writes FF does not define a feature flag actor,
|
||||
# it is currently not possible to increment the project statistics without enabling
|
||||
# or disabling it for all projects. The allow_git_http_fetch_writes FF allow us to control this.
|
||||
Feature.enabled?(:disable_git_http_fetch_writes) &&
|
||||
Feature.disabled?(:allow_git_http_fetch_writes, project, type: :beta)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ module WorkItemsHelper
|
|||
|
||||
def work_items_list_data(group, current_user)
|
||||
{
|
||||
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
|
||||
full_path: group.full_path,
|
||||
initial_sort: current_user&.user_preference&.issues_sort,
|
||||
is_signed_in: current_user.present?.to_s,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ module Users
|
|||
include IgnorableColumns
|
||||
|
||||
RELEASE_DAY = Date.new(2021, 5, 17)
|
||||
DAILY_VERIFICATION_LIMIT = 5
|
||||
|
||||
self.table_name = 'user_credit_card_validations'
|
||||
|
||||
|
|
@ -82,5 +83,16 @@ module Users
|
|||
def set_expiration_date_hash
|
||||
self.expiration_date_hash = Gitlab::CryptoHelper.sha256(expiration_date.to_s)
|
||||
end
|
||||
|
||||
def exceeded_daily_verification_limit?
|
||||
return false unless Feature.enabled?(:credit_card_validation_daily_limit, user, type: :gitlab_com_derisk)
|
||||
|
||||
duplicate_record_count = self.class
|
||||
.where(stripe_card_fingerprint: stripe_card_fingerprint)
|
||||
.where('credit_card_validated_at > ?', 24.hours.ago)
|
||||
.count
|
||||
|
||||
duplicate_record_count >= DAILY_VERIFICATION_LIMIT
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ module Users
|
|||
def execute
|
||||
credit_card = Users::CreditCardValidation.find_or_initialize_by_user(user_id)
|
||||
|
||||
credit_card_params = {
|
||||
credit_card_attributes = {
|
||||
credit_card_validated_at: credit_card_validated_at,
|
||||
last_digits: last_digits,
|
||||
holder_name: holder_name,
|
||||
|
|
@ -23,7 +23,11 @@ module Users
|
|||
stripe_card_fingerprint: stripe_card_fingerprint
|
||||
}
|
||||
|
||||
credit_card.update!(credit_card_params)
|
||||
credit_card.assign_attributes(credit_card_attributes)
|
||||
|
||||
return blocked if credit_card.exceeded_daily_verification_limit?
|
||||
|
||||
credit_card.save!
|
||||
|
||||
success
|
||||
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::NotNullViolation, ActiveRecord::RecordInvalid
|
||||
|
|
@ -85,5 +89,9 @@ module Users
|
|||
def error
|
||||
ServiceResponse.error(message: _('Error saving credit card validation record'))
|
||||
end
|
||||
|
||||
def blocked
|
||||
ServiceResponse.error(message: 'Credit card verification limit exceeded', reason: :rate_limited)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: allow_git_http_fetch_writes
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/426270
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159835
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/473133
|
||||
milestone: '17.3'
|
||||
group: group::source code
|
||||
type: beta
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
name: credit_card_validation_daily_limit
|
||||
feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/742
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/159151
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/472122
|
||||
milestone: '17.3'
|
||||
group: group::anti-abuse
|
||||
type: gitlab_com_derisk
|
||||
default_enabled: false
|
||||
|
|
@ -1320,8 +1320,10 @@ module API
|
|||
|
||||
if service.success?
|
||||
present user.credit_card_validation, with: Entities::UserCreditCardValidations
|
||||
elsif service.reason == :rate_limited
|
||||
render_api_error!(service.message, 400)
|
||||
else
|
||||
render_api_error!('400 Bad Request', 400)
|
||||
bad_request!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -1342,13 +1344,13 @@ module API
|
|||
|
||||
attrs = declared_params(include_missing: false)
|
||||
|
||||
render_api_error!('400 Bad Request', 400) unless attrs
|
||||
bad_request! unless attrs
|
||||
|
||||
service = ::UserPreferences::UpdateService.new(current_user, attrs).execute
|
||||
if service.success?
|
||||
present preferences, with: Entities::UserPreferences
|
||||
else
|
||||
render_api_error!('400 Bad Request', 400)
|
||||
bad_request!
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8428,6 +8428,9 @@ msgstr ""
|
|||
msgid "BillingPlan|Upgrade"
|
||||
msgstr ""
|
||||
|
||||
msgid "Billings|Credit card has exceeded the daily verification limit. Use a different card or try again later."
|
||||
msgstr ""
|
||||
|
||||
msgid "Billings|Error validating card details"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'increments fetch statistics' do
|
||||
it 'calls Projects::FetchStatisticsIncrementService service' do
|
||||
expect(Projects::FetchStatisticsIncrementService).to receive(:new).with(project).and_call_original
|
||||
|
||||
send_request
|
||||
end
|
||||
end
|
||||
|
||||
context 'when repository container is a project' do
|
||||
it_behaves_like described_class do
|
||||
let(:container) { project }
|
||||
|
|
@ -111,10 +119,46 @@ RSpec.describe Repositories::GitHttpController, feature_category: :source_code_m
|
|||
stub_feature_flags(disable_git_http_fetch_writes: true)
|
||||
end
|
||||
|
||||
it 'does not increment statistics' do
|
||||
expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
|
||||
context 'and allow_git_http_fetch_writes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(allow_git_http_fetch_writes: false)
|
||||
end
|
||||
|
||||
send_request
|
||||
it 'does not increment statistics' do
|
||||
expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
|
||||
|
||||
send_request
|
||||
end
|
||||
end
|
||||
|
||||
context 'and allow_git_http_fetch_writes is enabled' do
|
||||
before do
|
||||
stub_feature_flags(allow_git_http_fetch_writes: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'increments fetch statistics'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when disable_git_http_fetch_writes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(disable_git_http_fetch_writes: false)
|
||||
end
|
||||
|
||||
context 'and allow_git_http_fetch_writes is disabled' do
|
||||
before do
|
||||
stub_feature_flags(allow_git_http_fetch_writes: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'increments fetch statistics'
|
||||
end
|
||||
|
||||
context 'and allow_git_http_fetch_writes is enabled' do
|
||||
before do
|
||||
stub_feature_flags(allow_git_http_fetch_writes: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'increments fetch statistics'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
|
|||
let_it_be(:user2) { create(:user) }
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:sub_group) { create(:group, parent: group) }
|
||||
let_it_be(:project) { create(:project, :public, group: group, developers: [user1, user2]) }
|
||||
let_it_be(:sub_group_project) { create(:project, :public, group: sub_group, developers: [user1, user2]) }
|
||||
|
||||
let_it_be(:label1) { create(:label, project: project) }
|
||||
let_it_be(:label2) { create(:label, project: project) }
|
||||
|
|
@ -18,24 +20,38 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
|
|||
let_it_be(:milestone2) { create(:milestone, group: group, start_date: 2.days.from_now, due_date: 9.days.from_now) }
|
||||
|
||||
let_it_be(:incident) do
|
||||
create(:incident, project: project, assignees: [user1], author: user1, labels: [label1], description: 'aaa')
|
||||
create(:incident, project: project,
|
||||
assignees: [user1],
|
||||
author: user1,
|
||||
description: 'aaa',
|
||||
labels: [label1])
|
||||
end
|
||||
|
||||
let_it_be(:issue) do
|
||||
create(:issue, project: project, author: user1, labels: [label1, label2], milestone: milestone1, title: 'eee')
|
||||
create(:issue, project: project,
|
||||
author: user1,
|
||||
labels: [label1, label2],
|
||||
milestone: milestone1,
|
||||
title: 'eee')
|
||||
end
|
||||
|
||||
let_it_be(:task) do
|
||||
create(:work_item, :task, project: project, assignees: [user2], author: user2, milestone: milestone2)
|
||||
create(:work_item, :task, project: sub_group_project,
|
||||
assignees: [user2],
|
||||
author: user2,
|
||||
confidential: true,
|
||||
milestone: milestone2)
|
||||
end
|
||||
|
||||
let_it_be(:award_emoji) { create(:award_emoji, :upvote, user: user1, awardable: issue) }
|
||||
|
||||
context 'for signed in user' do
|
||||
before do
|
||||
sign_in(user1)
|
||||
visit group_work_items_path(group)
|
||||
end
|
||||
|
||||
describe 'assignees' do
|
||||
describe 'assignee' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Assignee', '=', user1.username, submit: true
|
||||
|
||||
|
|
@ -101,7 +117,33 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
|
|||
end
|
||||
end
|
||||
|
||||
describe 'labels' do
|
||||
describe 'confidential' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Confidential', 'Yes', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 1)
|
||||
expect(page).to have_link(task.title)
|
||||
|
||||
click_button 'Clear'
|
||||
|
||||
select_tokens 'Confidential', 'No', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 2)
|
||||
expect(page).to have_link(incident.title)
|
||||
expect(page).to have_link(issue.title)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'group' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Group', sub_group.name, submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 1)
|
||||
expect(page).to have_link(task.title)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'label' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Label', '=', label1.title, submit: true
|
||||
|
||||
|
|
@ -141,7 +183,7 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
|
|||
end
|
||||
end
|
||||
|
||||
describe 'milestones' do
|
||||
describe 'milestone' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Milestone', '=', milestone1.title, submit: true
|
||||
|
||||
|
|
@ -187,6 +229,38 @@ RSpec.describe 'Work items list filters', :js, feature_category: :team_planning
|
|||
end
|
||||
end
|
||||
|
||||
describe 'my-reaction' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'My-Reaction', '=', 'thumbsup', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 1)
|
||||
expect(page).to have_link(issue.title)
|
||||
|
||||
click_button 'Clear'
|
||||
|
||||
select_tokens 'My-Reaction', '!=', 'thumbsup', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 2)
|
||||
expect(page).to have_link(incident.title)
|
||||
expect(page).to have_link(task.title)
|
||||
|
||||
click_button 'Clear'
|
||||
|
||||
select_tokens 'My-Reaction', '=', 'None', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 2)
|
||||
expect(page).to have_link(incident.title)
|
||||
expect(page).to have_link(task.title)
|
||||
|
||||
click_button 'Clear'
|
||||
|
||||
select_tokens 'My-Reaction', '=', 'Any', submit: true
|
||||
|
||||
expect(page).to have_css('.issue', count: 1)
|
||||
expect(page).to have_link(issue.title)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'search within' do
|
||||
it 'filters', :aggregate_failures do
|
||||
select_tokens 'Search Within', 'Titles'
|
||||
|
|
|
|||
|
|
@ -23,8 +23,11 @@ import {
|
|||
OPERATOR_IS,
|
||||
TOKEN_TYPE_ASSIGNEE,
|
||||
TOKEN_TYPE_AUTHOR,
|
||||
TOKEN_TYPE_CONFIDENTIAL,
|
||||
TOKEN_TYPE_GROUP,
|
||||
TOKEN_TYPE_LABEL,
|
||||
TOKEN_TYPE_MILESTONE,
|
||||
TOKEN_TYPE_MY_REACTION,
|
||||
TOKEN_TYPE_SEARCH_WITHIN,
|
||||
TOKEN_TYPE_TYPE,
|
||||
} from '~/vue_shared/components/filtered_search_bar/constants';
|
||||
|
|
@ -60,6 +63,7 @@ describe('WorkItemsListApp component', () => {
|
|||
[setSortPreferenceMutation, sortPreferenceMutationResponse],
|
||||
]),
|
||||
provide: {
|
||||
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
|
||||
fullPath: 'full/path',
|
||||
initialSort: CREATED_DESC,
|
||||
isGroup: true,
|
||||
|
|
@ -120,6 +124,7 @@ describe('WorkItemsListApp component', () => {
|
|||
it('calls query to fetch work items', () => {
|
||||
expect(defaultQueryHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'full/path',
|
||||
includeDescendants: true,
|
||||
sort: CREATED_DESC,
|
||||
state: STATUS_OPEN,
|
||||
firstPageSize: 20,
|
||||
|
|
@ -154,6 +159,7 @@ describe('WorkItemsListApp component', () => {
|
|||
|
||||
expect(defaultQueryHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'full/path',
|
||||
includeDescendants: true,
|
||||
sort: CREATED_DESC,
|
||||
state: STATUS_OPEN,
|
||||
firstPageSize: 20,
|
||||
|
|
@ -225,8 +231,11 @@ describe('WorkItemsListApp component', () => {
|
|||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_GROUP },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_SEARCH_WITHIN },
|
||||
{ type: TOKEN_TYPE_TYPE },
|
||||
]);
|
||||
|
|
@ -240,8 +249,11 @@ describe('WorkItemsListApp component', () => {
|
|||
expect(findIssuableList().props('searchTokens')).toMatchObject([
|
||||
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
|
||||
{ type: TOKEN_TYPE_CONFIDENTIAL },
|
||||
{ type: TOKEN_TYPE_GROUP },
|
||||
{ type: TOKEN_TYPE_LABEL },
|
||||
{ type: TOKEN_TYPE_MILESTONE },
|
||||
{ type: TOKEN_TYPE_MY_REACTION },
|
||||
{ type: TOKEN_TYPE_SEARCH_WITHIN },
|
||||
]);
|
||||
});
|
||||
|
|
@ -276,6 +288,7 @@ describe('WorkItemsListApp component', () => {
|
|||
|
||||
expect(defaultQueryHandler).toHaveBeenCalledWith({
|
||||
fullPath: 'full/path',
|
||||
includeDescendants: true,
|
||||
sort: CREATED_DESC,
|
||||
state: STATUS_OPEN,
|
||||
search: 'find issues',
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ RSpec.describe WorkItemsHelper, feature_category: :team_planning do
|
|||
it 'returns expected data' do
|
||||
expect(work_items_list_data).to include(
|
||||
{
|
||||
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
|
||||
full_path: group.full_path,
|
||||
initial_sort: current_user&.user_preference&.issues_sort,
|
||||
is_signed_in: current_user.present?.to_s,
|
||||
|
|
|
|||
|
|
@ -317,5 +317,45 @@ RSpec.describe Users::CreditCardValidation, feature_category: :user_profile do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#exceeded_daily_verification_limit?' do
|
||||
let(:credit_card_validation) { build(:credit_card_validation) }
|
||||
|
||||
subject(:exceeded_limit?) { credit_card_validation.exceeded_daily_verification_limit? }
|
||||
|
||||
before do
|
||||
stub_const("#{described_class}::DAILY_VERIFICATION_LIMIT", 1)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
|
||||
context 'when the limit has been exceeded' do
|
||||
before do
|
||||
create(:credit_card_validation, stripe_card_fingerprint: credit_card_validation.stripe_card_fingerprint)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(true) }
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(credit_card_validation_daily_limit: false)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the limit is exceeded but records have credit_card_validated_at > 24 hours' do
|
||||
before do
|
||||
create(
|
||||
:credit_card_validation,
|
||||
stripe_card_fingerprint: credit_card_validation.stripe_card_fingerprint,
|
||||
credit_card_validated_at: 25.hours.ago
|
||||
)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2205,6 +2205,35 @@ RSpec.describe API::Users, :aggregate_failures, feature_category: :user_manageme
|
|||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(json_response['message']).to eq('404 User Not Found')
|
||||
end
|
||||
|
||||
context 'when the credit card daily verification limit has been exceeded' do
|
||||
before do
|
||||
stub_const("Users::CreditCardValidation::DAILY_VERIFICATION_LIMIT", 1)
|
||||
create(:credit_card_validation, stripe_card_fingerprint: stripe_card_fingerprint)
|
||||
end
|
||||
|
||||
it "returns a 400 error with the reason" do
|
||||
put api(path, admin, admin_mode: true), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']).to eq('Credit card verification limit exceeded')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when UpsertCreditCardValidationService returns an unexpected error' do
|
||||
before do
|
||||
allow_next_instance_of(::Users::UpsertCreditCardValidationService) do |instance|
|
||||
allow(instance).to receive(:execute).and_return(ServiceResponse.error(message: 'upsert failed'))
|
||||
end
|
||||
end
|
||||
|
||||
it "returns a generic 400 error" do
|
||||
put api(path, admin, admin_mode: true), params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']).to eq('400 Bad request')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -198,5 +198,21 @@ RSpec.describe Users::UpsertCreditCardValidationService, feature_category: :user
|
|||
expect(service_result.message).to eq(_('Error saving credit card validation record'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the credit card verification limit has been reached' do
|
||||
before do
|
||||
allow_next_instance_of(Users::CreditCardValidation) do |instance|
|
||||
allow(instance).to receive(:exceeded_daily_verification_limit?).and_return(true)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns an error', :aggregate_failures do
|
||||
service_result = service.execute
|
||||
|
||||
expect(service_result).to be_error
|
||||
expect(service_result.message).to eq('Credit card verification limit exceeded')
|
||||
expect(service_result.reason).to eq(:rate_limited)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue