diff --git a/Gemfile b/Gemfile index e72b9263c05..0213bc0af89 100644 --- a/Gemfile +++ b/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 diff --git a/Gemfile.checksum b/Gemfile.checksum index 3d55ff7cb69..10a8b5d6d18 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -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"}, diff --git a/Gemfile.lock b/Gemfile.lock index 12f9c12e397..450f742bff2 100644 --- a/Gemfile.lock +++ b/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) diff --git a/app/assets/javascripts/boards/graphql/sub_groups.query.graphql b/app/assets/javascripts/boards/graphql/sub_groups.query.graphql new file mode 100644 index 00000000000..13f228e39b0 --- /dev/null +++ b/app/assets/javascripts/boards/graphql/sub_groups.query.graphql @@ -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 + } + } + } +} diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 7aae0a325c3..5f56b3e700a 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -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', diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue new file mode 100644 index 00000000000..8c58b5f723f --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/group_token.vue @@ -0,0 +1,109 @@ + + + diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue index 0899d4073d8..9b656d771b3 100644 --- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue +++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue @@ -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({ diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js index 0648347d6a1..8a41b8adbc5 100644 --- a/app/assets/javascripts/work_items/list/index.js +++ b/app/assets/javascripts/work_items/list/index.js @@ -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), diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql index 2ac0762315c..59918d650bc 100644 --- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql +++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql @@ -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 diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb index 85a996a8f52..995cfe26e6a 100644 --- a/app/controllers/repositories/git_http_controller.rb +++ b/app/controllers/repositories/git_http_controller.rb @@ -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 diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb index ec9aed59171..1cbf872cb45 100644 --- a/app/helpers/work_items_helper.rb +++ b/app/helpers/work_items_helper.rb @@ -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, diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb index f12c3767cf6..40e950b919e 100644 --- a/app/models/users/credit_card_validation.rb +++ b/app/models/users/credit_card_validation.rb @@ -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 diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb index 7e24bfcfee0..a49f8c10032 100644 --- a/app/services/users/upsert_credit_card_validation_service.rb +++ b/app/services/users/upsert_credit_card_validation_service.rb @@ -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 diff --git a/config/feature_flags/beta/allow_git_http_fetch_writes.yml b/config/feature_flags/beta/allow_git_http_fetch_writes.yml new file mode 100644 index 00000000000..7fc7b492cd3 --- /dev/null +++ b/config/feature_flags/beta/allow_git_http_fetch_writes.yml @@ -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 diff --git a/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml b/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml new file mode 100644 index 00000000000..302f919c9a2 --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/credit_card_validation_daily_limit.yml @@ -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 diff --git a/lib/api/users.rb b/lib/api/users.rb index 30301673f1b..00a6f5be0aa 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9ff526654f9..f092dfbe4b0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index d3f639c639d..519a4772f96 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -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 diff --git a/spec/features/projects/work_items/work_items_list_filters_spec.rb b/spec/features/projects/work_items/work_items_list_filters_spec.rb index 64b409cc9e9..60a10aeb08e 100644 --- a/spec/features/projects/work_items/work_items_list_filters_spec.rb +++ b/spec/features/projects/work_items/work_items_list_filters_spec.rb @@ -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' diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js index 43c175ab104..c19f745f2d0 100644 --- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js +++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js @@ -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', diff --git a/spec/helpers/work_items_helper_spec.rb b/spec/helpers/work_items_helper_spec.rb index e41eeb7a112..f88c2d17014 100644 --- a/spec/helpers/work_items_helper_spec.rb +++ b/spec/helpers/work_items_helper_spec.rb @@ -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, diff --git a/spec/models/users/credit_card_validation_spec.rb b/spec/models/users/credit_card_validation_spec.rb index 9e86891acfc..0bf2fd6f48d 100644 --- a/spec/models/users/credit_card_validation_spec.rb +++ b/spec/models/users/credit_card_validation_spec.rb @@ -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 diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index df5800eca68..3a5fbe0a548 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -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 diff --git a/spec/services/users/upsert_credit_card_validation_service_spec.rb b/spec/services/users/upsert_credit_card_validation_service_spec.rb index 02e8cad4011..60459396c7e 100644 --- a/spec/services/users/upsert_credit_card_validation_service_spec.rb +++ b/spec/services/users/upsert_credit_card_validation_service_spec.rb @@ -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