From 96135034f442825a54cea2812192133d376fbc4b Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 7 May 2021 21:10:34 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../components/board_filtered_search.vue | 154 ++++++++++++++++++ .../dismiss_user_callout.mutation.graphql | 9 + .../javascripts/ide/components/file_alert.vue | 26 +++ .../ide/components/repo_editor.vue | 37 ++++- app/assets/javascripts/ide/index.js | 5 +- .../ide/lib/alerts/environments.vue | 32 ++++ .../javascripts/ide/lib/alerts/index.js | 20 +++ app/assets/javascripts/ide/services/gql.js | 1 + app/assets/javascripts/ide/services/index.js | 16 +- app/assets/javascripts/ide/stores/actions.js | 3 +- .../javascripts/ide/stores/actions/alert.js | 18 ++ app/assets/javascripts/ide/stores/getters.js | 2 + .../javascripts/ide/stores/getters/alert.js | 3 + .../javascripts/ide/stores/mutation_types.js | 5 + .../javascripts/ide/stores/mutations.js | 2 + .../javascripts/ide/stores/mutations/alert.js | 21 +++ app/assets/javascripts/ide/stores/state.js | 2 + ...guidance_environments_webide_experiment.rb | 15 ++ app/helpers/ide_helper.rb | 15 +- app/models/group.rb | 2 + app/models/user_callout.rb | 3 +- .../in_product_marketing_emails_service.rb | 14 +- app/services/spam/spam_verdict_service.rb | 23 ++- .../projects/_import_project_pane.html.haml | 2 +- app/views/shared/_import_form.html.haml | 10 +- .../shared/issuable/_search_bar.html.haml | 2 +- ...-in-the-url-when-importing-repo-by-url.yml | 5 + ...n_product_guidance_environments_webide.yml | 8 + doc/api/graphql/reference/index.md | 1 + lib/gitlab/spamcheck/client.rb | 2 +- locale/gitlab.pot | 6 + ...nce_environments_webide_experiment_spec.rb | 22 +++ spec/features/projects/new_project_spec.rb | 10 ++ .../components/board_filtered_search_spec.js | 146 +++++++++++++++++ .../ide/components/repo_editor_spec.js | 1 + .../ide/lib/alerts/environment_spec.js | 21 +++ spec/frontend/ide/services/index_spec.js | 33 +++- .../frontend/ide/stores/actions/alert_spec.js | 46 ++++++ spec/frontend/ide/stores/actions_spec.js | 19 ++- .../frontend/ide/stores/getters/alert_spec.js | 46 ++++++ .../ide/stores/mutations/alert_spec.js | 26 +++ spec/helpers/ide_helper_spec.rb | 30 ++++ spec/lib/gitlab/spamcheck/client_spec.rb | 10 +- spec/models/group_spec.rb | 10 ++ .../spam/spam_verdict_service_spec.rb | 81 +++++++-- 45 files changed, 916 insertions(+), 49 deletions(-) create mode 100644 app/assets/javascripts/boards/components/board_filtered_search.vue create mode 100644 app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql create mode 100644 app/assets/javascripts/ide/components/file_alert.vue create mode 100644 app/assets/javascripts/ide/lib/alerts/environments.vue create mode 100644 app/assets/javascripts/ide/lib/alerts/index.js create mode 100644 app/assets/javascripts/ide/stores/actions/alert.js create mode 100644 app/assets/javascripts/ide/stores/getters/alert.js create mode 100644 app/assets/javascripts/ide/stores/mutations/alert.js create mode 100644 app/experiments/in_product_guidance_environments_webide_experiment.rb create mode 100644 changelogs/unreleased/276953-enforce-git-in-the-url-when-importing-repo-by-url.yml create mode 100644 config/feature_flags/experiment/in_product_guidance_environments_webide.yml create mode 100644 spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb create mode 100644 spec/frontend/boards/components/board_filtered_search_spec.js create mode 100644 spec/frontend/ide/lib/alerts/environment_spec.js create mode 100644 spec/frontend/ide/stores/actions/alert_spec.js create mode 100644 spec/frontend/ide/stores/getters/alert_spec.js create mode 100644 spec/frontend/ide/stores/mutations/alert_spec.js diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue new file mode 100644 index 00000000000..e564af0c353 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -0,0 +1,154 @@ + + + diff --git a/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql new file mode 100644 index 00000000000..2b831bf1338 --- /dev/null +++ b/app/assets/javascripts/graphql_shared/mutations/dismiss_user_callout.mutation.graphql @@ -0,0 +1,9 @@ +mutation dismissUserCallout($input: UserCalloutCreateInput!) { + userCalloutCreate(input: $input) { + errors + userCallout { + dismissedAt + featureName + } + } +} diff --git a/app/assets/javascripts/ide/components/file_alert.vue b/app/assets/javascripts/ide/components/file_alert.vue new file mode 100644 index 00000000000..2a894596bf4 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_alert.vue @@ -0,0 +1,26 @@ + + diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b57dcd4276c..bf2af9ffd49 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,4 +1,5 @@ + diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js new file mode 100644 index 00000000000..c9db9779b1f --- /dev/null +++ b/app/assets/javascripts/ide/lib/alerts/index.js @@ -0,0 +1,20 @@ +import { leftSidebarViews } from '../../constants'; +import EnvironmentsMessage from './environments.vue'; + +const alerts = [ + { + key: Symbol('ALERT_ENVIRONMENT'), + show: (state, file) => + state.currentActivityView === leftSidebarViews.commit.name && + file.path === '.gitlab-ci.yml' && + state.environmentsGuidanceAlertDetected && + !state.environmentsGuidanceAlertDismissed, + props: { variant: 'tip' }, + dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'), + message: EnvironmentsMessage, + }, +]; + +export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key; + +export const getAlert = (key) => alerts.find((x) => x.key === key); diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js index 89dda187360..c8c1031c0f3 100644 --- a/app/assets/javascripts/ide/services/gql.js +++ b/app/assets/javascripts/ide/services/gql.js @@ -18,3 +18,4 @@ const getClient = memoize(() => ); export const query = (...args) => getClient().query(...args); +export const mutate = (...args) => getClient().mutate(...args); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 0aa08323d13..6bd28cd4fb6 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,8 +1,10 @@ import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import axios from '~/lib/utils/axios_utils'; import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; -import { query } from './gql'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; +import { query, mutate } from './gql'; const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data); @@ -101,4 +103,16 @@ export default { const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`; return axios.post(url); }, + getCiConfig(projectPath, content) { + return query({ + query: ciConfig, + variables: { projectPath, content }, + }).then(({ data }) => data.ciConfig); + }, + dismissUserCallout(name) { + return mutate({ + mutation: dismissUserCallout, + variables: { input: { featureName: name } }, + }).then(({ data }) => data); + }, }; diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index bf94f9d31c8..062dc150805 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -17,7 +17,7 @@ import * as types from './mutation_types'; export const redirectToUrl = (self, url) => visitUrl(url); -export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); +export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); export const discardAllChanges = ({ state, commit, dispatch }) => { state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path)); @@ -316,3 +316,4 @@ export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; export * from './actions/merge_request'; +export * from './actions/alert'; diff --git a/app/assets/javascripts/ide/stores/actions/alert.js b/app/assets/javascripts/ide/stores/actions/alert.js new file mode 100644 index 00000000000..4c33dc19520 --- /dev/null +++ b/app/assets/javascripts/ide/stores/actions/alert.js @@ -0,0 +1,18 @@ +import service from '../../services'; +import { + DETECT_ENVIRONMENTS_GUIDANCE_ALERT, + DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, +} from '../mutation_types'; + +export const detectGitlabCiFileAlerts = ({ dispatch }, content) => + dispatch('detectEnvironmentsGuidance', content); + +export const detectEnvironmentsGuidance = ({ commit, state }, content) => + service.getCiConfig(state.currentProjectId, content).then((data) => { + commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages); + }); + +export const dismissEnvironmentsGuidance = ({ commit }) => + service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => { + commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT); + }); diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index e8b1a0ea494..3c02b1d1da7 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => { fileMatch: [`*${path}`], }; }; + +export * from './getters/alert'; diff --git a/app/assets/javascripts/ide/stores/getters/alert.js b/app/assets/javascripts/ide/stores/getters/alert.js new file mode 100644 index 00000000000..714e2d89b4f --- /dev/null +++ b/app/assets/javascripts/ide/stores/getters/alert.js @@ -0,0 +1,3 @@ +import { findAlertKeyToShow } from '../../lib/alerts'; + +export const getAlert = (state) => (file) => findAlertKeyToShow(state, file); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 76ba8339703..77755b179ef 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -70,3 +70,8 @@ export const RENAME_ENTRY = 'RENAME_ENTRY'; export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY'; export const RESTORE_TREE = 'RESTORE_TREE'; + +// Alert mutation types + +export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT'; +export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 576f861a090..48648796e66 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import * as types from './mutation_types'; +import alertMutations from './mutations/alert'; import branchMutations from './mutations/branch'; import fileMutations from './mutations/file'; import mergeRequestMutation from './mutations/merge_request'; @@ -244,4 +245,5 @@ export default { ...fileMutations, ...treeMutations, ...branchMutations, + ...alertMutations, }; diff --git a/app/assets/javascripts/ide/stores/mutations/alert.js b/app/assets/javascripts/ide/stores/mutations/alert.js new file mode 100644 index 00000000000..bb2d33a836b --- /dev/null +++ b/app/assets/javascripts/ide/stores/mutations/alert.js @@ -0,0 +1,21 @@ +import { + DETECT_ENVIRONMENTS_GUIDANCE_ALERT, + DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, +} from '../mutation_types'; + +export default { + [DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) { + if (!stages) { + return; + } + const hasEnvironments = stages?.nodes?.some((stage) => + stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)), + ); + const hasParsedCi = Array.isArray(stages.nodes); + + state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi; + }, + [DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) { + state.environmentsGuidanceAlertDismissed = true; + }, +}; diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index c1a83bf0726..83551e87f09 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -30,4 +30,6 @@ export default () => ({ renderWhitespaceInCode: false, editorTheme: DEFAULT_THEME, codesandboxBundlerUrl: null, + environmentsGuidanceAlertDismissed: false, + environmentsGuidanceAlertDetected: false, }); diff --git a/app/experiments/in_product_guidance_environments_webide_experiment.rb b/app/experiments/in_product_guidance_environments_webide_experiment.rb new file mode 100644 index 00000000000..d77063a9834 --- /dev/null +++ b/app/experiments/in_product_guidance_environments_webide_experiment.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass + exclude :has_environments? + + def control_behavior + false + end + + private + + def has_environments? + !context.project.environments.empty? + end +end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 61d8d0f779d..a38ab97e59c 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -17,7 +17,8 @@ module IdeHelper 'file-path' => @path, 'merge-request' => @merge_request, 'fork-info' => @fork_info&.to_json, - 'project' => convert_to_project_entity_json(@project) + 'project' => convert_to_project_entity_json(@project), + 'enable-environments-guidance' => enable_environments_guidance?.to_s } end @@ -28,6 +29,18 @@ module IdeHelper API::Entities::Project.represent(project).to_json end + + def enable_environments_guidance? + experiment(:in_product_guidance_environments_webide, project: @project) do |e| + e.try { !has_dismissed_ide_environments_callout? } + + e.run + end + end + + def has_dismissed_ide_environments_callout? + current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance') + end end ::IdeHelper.prepend_if_ee('::EE::IdeHelper') diff --git a/app/models/group.rb b/app/models/group.rb index aef5bdd6e88..b9bee1810a6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -107,6 +107,8 @@ class Group < Namespace scope :with_users, -> { includes(:users) } + scope :with_onboarding_progress, -> { joins(:onboarding_progress) } + scope :by_id, ->(groups) { where(id: groups) } scope :for_authorized_group_members, -> (user_ids) do diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb index 852ea05b77f..8fc9efddac9 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord unfinished_tag_cleanup_callout: 27, eoa_bronze_plan_banner: 28, # EE-only pipeline_needs_banner: 29, - pipeline_needs_hover_tip: 30 + pipeline_needs_hover_tip: 30, + web_ide_ci_environments_guidance: 31 } validates :user, presence: true diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb index eb81253bc08..3ddb4535bae 100644 --- a/app/services/namespaces/in_product_marketing_emails_service.rb +++ b/app/services/namespaces/in_product_marketing_emails_service.rb @@ -66,7 +66,6 @@ module Namespaces Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group) end - # rubocop: disable CodeReuse/ActiveRecord def groups_for_track onboarding_progress_scope = OnboardingProgress .completed_actions_with_latest_in_range(completed_actions, range) @@ -75,9 +74,18 @@ module Namespaces # Filtering out sub-groups is a temporary fix to prevent calling # `.root_ancestor` on groups that are not root groups. # See https://gitlab.com/groups/gitlab-org/-/epics/5594 for more information. - Group.where(parent_id: nil).joins(:onboarding_progress).merge(onboarding_progress_scope) + Group + .top_most + .with_onboarding_progress + .merge(onboarding_progress_scope) + .merge(subscription_scope) end + def subscription_scope + {} + end + + # rubocop: disable CodeReuse/ActiveRecord def users_for_group(group) group.users .where(email_opted_in: true) @@ -136,3 +144,5 @@ module Namespaces end end end + +Namespaces::InProductMarketingEmailsService.prepend_ee_mod diff --git a/app/services/spam/spam_verdict_service.rb b/app/services/spam/spam_verdict_service.rb index 8dbc590314c..32e58fcc06b 100644 --- a/app/services/spam/spam_verdict_service.rb +++ b/app/services/spam/spam_verdict_service.rb @@ -15,11 +15,17 @@ module Spam def execute spamcheck_result = nil + spamcheck_attribs = {} external_spam_check_round_trip_time = Benchmark.realtime do - spamcheck_result = spamcheck_verdict + spamcheck_result, spamcheck_attribs = spamcheck_verdict end + # assign result to a var and log it before reassigning to nil when monitorMode is true + original_spamcheck_result = spamcheck_result + + spamcheck_result = nil if spamcheck_attribs&.fetch("monitorMode", "false") == "true" + akismet_result = akismet_verdict # filter out anything we don't recognise, including nils. @@ -33,7 +39,8 @@ module Spam logger.info(class: self.class.name, akismet_verdict: akismet_verdict, - spam_check_verdict: spamcheck_result, + spam_check_verdict: original_spamcheck_result, + extra_attributes: spamcheck_attribs, spam_check_rtt: external_spam_check_round_trip_time.real, final_verdict: final_verdict, username: user.username, @@ -61,21 +68,23 @@ module Spam return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled begin - result, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context) - return unless result + result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context) + return [nil, attribs] unless result # @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545 + return [result, attribs] if result == NOOP || attribs["monitorMode"] == "true" + # Duplicate logic with Akismet logic in #akismet_verdict if Gitlab::Recaptcha.enabled? && result != ALLOW - CONDITIONAL_ALLOW + [CONDITIONAL_ALLOW, attribs] else - result + [result, attribs] end rescue StandardError => e Gitlab::ErrorTracking.log_exception(e) # Default to ALLOW if any errors occur - ALLOW + [ALLOW, attribs] end end diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index e6ded3ad912..c0fe788b56a 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -83,7 +83,7 @@ .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } - = form_for @project, html: { class: 'new_project' } do |f| + = form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f| %hr = render "shared/import_form", f: f = render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml index 65e02341936..cf9ee1a5231 100644 --- a/app/views/shared/_import_form.html.haml +++ b/app/views/shared/_import_form.html.haml @@ -6,8 +6,14 @@ = f.label :import_url, class: 'label-bold' do %span = _('Git repository URL') - = f.text_field :import_url, value: import_url.sanitized_url, - autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true + = f.text_field :import_url, + value: import_url.sanitized_url, + autocomplete: 'off', + class: 'form-control gl-form-input', + placeholder: 'https://gitlab.company.com/group/project.git', + required: true, + pattern: '(?:git|https?):\/\/.*/.*\.git$', + title: _('Please provide a valid URL ending with .git') .row .form-group.col-md-6 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 1e340f033a1..831fe784acf 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -22,7 +22,7 @@ .check-all-holder.d-none.d-sm-block.hidden = check_box_tag "check-all-issues", nil, false, class: "check-all-issues left" - if Feature.enabled?(:boards_filtered_search, @group) && is_epic_board - #js-board-filtered-search + #js-board-filtered-search{ data: { full_path: @group&.full_path } } - else .issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row .filtered-search-box diff --git a/changelogs/unreleased/276953-enforce-git-in-the-url-when-importing-repo-by-url.yml b/changelogs/unreleased/276953-enforce-git-in-the-url-when-importing-repo-by-url.yml new file mode 100644 index 00000000000..98ce550e073 --- /dev/null +++ b/changelogs/unreleased/276953-enforce-git-in-the-url-when-importing-repo-by-url.yml @@ -0,0 +1,5 @@ +--- +title: Enforce .git suffix when importing git repo +merge_request: 61115 +author: +type: changed diff --git a/config/feature_flags/experiment/in_product_guidance_environments_webide.yml b/config/feature_flags/experiment/in_product_guidance_environments_webide.yml new file mode 100644 index 00000000000..4eaf6e90b27 --- /dev/null +++ b/config/feature_flags/experiment/in_product_guidance_environments_webide.yml @@ -0,0 +1,8 @@ +--- +name: in_product_guidance_environments_webide +introduced_by_url: +rollout_issue_url: +milestone: '13.12' +type: experiment +group: group::release +default_enabled: false diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 565f2da07eb..a1c397890ef 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14418,6 +14418,7 @@ Name of the feature that the callout is for. | `UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. | | `WEBHOOKS_MOVED` | Callout feature name for webhooks_moved. | | `WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. | +| `WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. | ### `UserState` diff --git a/lib/gitlab/spamcheck/client.rb b/lib/gitlab/spamcheck/client.rb index e4dfb3da0f3..6afc21be4e0 100644 --- a/lib/gitlab/spamcheck/client.rb +++ b/lib/gitlab/spamcheck/client.rb @@ -45,7 +45,7 @@ module Gitlab metadata: { 'authorization' => Gitlab::CurrentSettings.spam_check_api_key }) verdict = convert_verdict_to_gitlab_constant(response.verdict) - [verdict, response.error] + [verdict, response.extra_attributes.to_h, response.error] end private diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 93235d47b2d..55356dba1b6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22044,6 +22044,9 @@ msgstr "" msgid "No data to display" msgstr "" +msgid "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}" +msgstr "" + msgid "No deployments found" msgstr "" @@ -24322,6 +24325,9 @@ msgstr "" msgid "Please provide a valid URL" msgstr "" +msgid "Please provide a valid URL ending with .git" +msgstr "" + msgid "Please provide a valid YouTube URL or ID" msgstr "" diff --git a/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb b/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb new file mode 100644 index 00000000000..d616672173e --- /dev/null +++ b/spec/experiments/in_product_guidance_environments_webide_experiment_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe InProductGuidanceEnvironmentsWebideExperiment, :experiment do + subject { described_class.new(project: project) } + + let(:project) { create(:project, :repository) } + + before do + stub_experiments(in_product_guidance_environments_webide: :candidate) + end + + it 'excludes projects with environments' do + create(:environment, project: project) + expect(subject).to exclude(project: project) + end + + it 'does not exlude projects without environments' do + expect(subject).not_to exclude(project: project) + end +end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index e1fbe2da9f1..ba28338cae3 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -355,6 +355,16 @@ RSpec.describe 'New project', :js do expect(git_import_instructions).to have_content 'Git repository URL' end + it 'reports error if repo URL does not end with .git' do + fill_in 'project_import_url', with: 'http://foo/bar' + fill_in 'project_name', with: 'import-project-without-git-suffix' + fill_in 'project_path', with: 'import-project-without-git-suffix' + + click_button 'Create project' + + expect(page).to have_text('Please provide a valid URL ending with .git') + end + it 'keeps "Import project" tab open after form validation error' do collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace) diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js new file mode 100644 index 00000000000..e27badca9de --- /dev/null +++ b/spec/frontend/boards/components/board_filtered_search_spec.js @@ -0,0 +1,146 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import Vuex from 'vuex'; +import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; +import { createStore } from '~/boards/stores'; +import * as urlUtility from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; + +Vue.use(Vuex); + +describe('BoardFilteredSearch', () => { + let wrapper; + let store; + const tokens = [ + { + icon: 'labels', + title: __('Label'), + type: 'label_name', + operators: [ + { value: '=', description: 'is' }, + { value: '!=', description: 'is not' }, + ], + token: LabelToken, + unique: false, + symbol: '~', + fetchLabels: () => new Promise(() => {}), + }, + { + icon: 'pencil', + title: __('Author'), + type: 'author_username', + operators: [ + { value: '=', description: 'is' }, + { value: '!=', description: 'is not' }, + ], + symbol: '@', + token: AuthorToken, + unique: true, + fetchAuthors: () => new Promise(() => {}), + }, + ]; + + const createComponent = ({ initialFilterParams = {} } = {}) => { + wrapper = shallowMount(BoardFilteredSearch, { + provide: { initialFilterParams, fullPath: '' }, + store, + propsData: { + tokens, + }, + }); + }; + + const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot); + + beforeEach(() => { + // this needed for actions call for performSearch + window.gon = { features: {} }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('default', () => { + beforeEach(() => { + store = createStore(); + + jest.spyOn(store, 'dispatch'); + + createComponent(); + }); + + it('renders FilteredSearch', () => { + expect(findFilteredSearch().exists()).toBe(true); + }); + + it('passes the correct tokens to FilteredSearch', () => { + expect(findFilteredSearch().props('tokens')).toEqual(tokens); + }); + + describe('when onFilter is emitted', () => { + it('calls performSearch', () => { + findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]); + + expect(store.dispatch).toHaveBeenCalledWith('performSearch'); + }); + + it('calls historyPushState', () => { + jest.spyOn(urlUtility, 'updateHistory'); + findFilteredSearch().vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]); + + expect(urlUtility.updateHistory).toHaveBeenCalledWith({ + replace: true, + title: '', + url: 'http://test.host/', + }); + }); + }); + }); + + describe('when searching', () => { + beforeEach(() => { + store = createStore(); + + jest.spyOn(store, 'dispatch'); + + createComponent(); + }); + + it('sets the url params to the correct results', async () => { + const mockFilters = [ + { type: 'author_username', value: { data: 'root', operator: '=' } }, + { type: 'label_name', value: { data: 'label', operator: '=' } }, + { type: 'label_name', value: { data: 'label2', operator: '=' } }, + ]; + jest.spyOn(urlUtility, 'updateHistory'); + findFilteredSearch().vm.$emit('onFilter', mockFilters); + + expect(urlUtility.updateHistory).toHaveBeenCalledWith({ + title: '', + replace: true, + url: 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2', + }); + }); + }); + + describe('when url params are already set', () => { + beforeEach(() => { + store = createStore(); + + jest.spyOn(store, 'dispatch'); + + createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } }); + }); + + it('passes the correct props to FilterSearchBar', () => { + expect(findFilteredSearch().props('initialFilterValue')).toEqual([ + { type: 'author_username', value: { data: 'root', operator: '=' } }, + { type: 'label_name', value: { data: 'label', operator: '=' } }, + ]); + }); + }); +}); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index a3b327343e5..646e51160d8 100644 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -510,6 +510,7 @@ describe('RepoEditor', () => { }, }); await vm.$nextTick(); + await vm.$nextTick(); expect(vm.initEditor).toHaveBeenCalled(); }); diff --git a/spec/frontend/ide/lib/alerts/environment_spec.js b/spec/frontend/ide/lib/alerts/environment_spec.js new file mode 100644 index 00000000000..d645209345c --- /dev/null +++ b/spec/frontend/ide/lib/alerts/environment_spec.js @@ -0,0 +1,21 @@ +import { GlLink } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Environments from '~/ide/lib/alerts/environments.vue'; + +describe('~/ide/lib/alerts/environment.vue', () => { + let wrapper; + + beforeEach(() => { + wrapper = mount(Environments); + }); + + it('shows a message regarding environments', () => { + expect(wrapper.text()).toBe( + "No deployments detected. Use environments to control your software's continuous deployment. Learn more about deployment jobs.", + ); + }); + + it('links to the help page on environments', () => { + expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/help/ci/environments/index.md'); + }); +}); diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js index 3503834e24b..4a726cff3b6 100644 --- a/spec/frontend/ide/services/index_spec.js +++ b/spec/frontend/ide/services/index_spec.js @@ -2,9 +2,11 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; import Api from '~/api'; +import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; import services from '~/ide/services'; -import { query } from '~/ide/services/gql'; +import { query, mutate } from '~/ide/services/gql'; import { escapeFileUrl } from '~/lib/utils/url_utility'; +import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql'; import { projectData } from '../mock_data'; jest.mock('~/api'); @@ -299,4 +301,33 @@ describe('IDE services', () => { }); }); }); + describe('getCiConfig', () => { + const TEST_PROJECT_PATH = 'foo/bar'; + const TEST_CI_CONFIG = 'test config'; + + it('queries with the given CI config and project', () => { + const result = { data: { ciConfig: { test: 'data' } } }; + query.mockResolvedValue(result); + return services.getCiConfig(TEST_PROJECT_PATH, TEST_CI_CONFIG).then((data) => { + expect(data).toEqual(result.data.ciConfig); + expect(query).toHaveBeenCalledWith({ + query: ciConfig, + variables: { projectPath: TEST_PROJECT_PATH, content: TEST_CI_CONFIG }, + }); + }); + }); + }); + describe('dismissUserCallout', () => { + it('mutates the callout to dismiss', () => { + const result = { data: { callouts: { test: 'data' } } }; + mutate.mockResolvedValue(result); + return services.dismissUserCallout('test').then((data) => { + expect(data).toEqual(result.data); + expect(mutate).toHaveBeenCalledWith({ + mutation: dismissUserCallout, + variables: { input: { featureName: 'test' } }, + }); + }); + }); + }); }); diff --git a/spec/frontend/ide/stores/actions/alert_spec.js b/spec/frontend/ide/stores/actions/alert_spec.js new file mode 100644 index 00000000000..1321c402ebb --- /dev/null +++ b/spec/frontend/ide/stores/actions/alert_spec.js @@ -0,0 +1,46 @@ +import testAction from 'helpers/vuex_action_helper'; +import service from '~/ide/services'; +import { + detectEnvironmentsGuidance, + dismissEnvironmentsGuidance, +} from '~/ide/stores/actions/alert'; +import * as types from '~/ide/stores/mutation_types'; + +jest.mock('~/ide/services'); + +describe('~/ide/stores/actions/alert', () => { + describe('detectEnvironmentsGuidance', () => { + it('should try to fetch CI info', () => { + const stages = ['a', 'b', 'c']; + service.getCiConfig.mockResolvedValue({ stages }); + + return testAction( + detectEnvironmentsGuidance, + 'the content', + { currentProjectId: 'gitlab/test' }, + [{ type: types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, payload: stages }], + [], + () => expect(service.getCiConfig).toHaveBeenCalledWith('gitlab/test', 'the content'), + ); + }); + }); + describe('dismissCallout', () => { + it('should try to dismiss the given callout', () => { + const callout = { featureName: 'test', dismissedAt: 'now' }; + + service.dismissUserCallout.mockResolvedValue({ userCalloutCreate: { userCallout: callout } }); + + return testAction( + dismissEnvironmentsGuidance, + undefined, + {}, + [{ type: types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT }], + [], + () => + expect(service.dismissUserCallout).toHaveBeenCalledWith( + 'web_ide_ci_environments_guidance', + ), + ); + }); + }); +}); diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index d47dd88dd47..ad55313da93 100644 --- a/spec/frontend/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -4,6 +4,7 @@ import eventHub from '~/ide/eventhub'; import { createRouter } from '~/ide/ide_router'; import { createStore } from '~/ide/stores'; import { + init, stageAllChanges, unstageAllChanges, toggleFileFinder, @@ -54,15 +55,15 @@ describe('Multi-file store actions', () => { }); }); - describe('setInitialData', () => { - it('commits initial data', (done) => { - store - .dispatch('setInitialData', { canCommit: true }) - .then(() => { - expect(store.state.canCommit).toBeTruthy(); - done(); - }) - .catch(done.fail); + describe('init', () => { + it('commits initial data and requests user callouts', () => { + return testAction( + init, + { canCommit: true }, + store.state, + [{ type: 'SET_INITIAL_DATA', payload: { canCommit: true } }], + [], + ); }); }); diff --git a/spec/frontend/ide/stores/getters/alert_spec.js b/spec/frontend/ide/stores/getters/alert_spec.js new file mode 100644 index 00000000000..7068b8e637f --- /dev/null +++ b/spec/frontend/ide/stores/getters/alert_spec.js @@ -0,0 +1,46 @@ +import { getAlert } from '~/ide/lib/alerts'; +import EnvironmentsMessage from '~/ide/lib/alerts/environments.vue'; +import { createStore } from '~/ide/stores'; +import * as getters from '~/ide/stores/getters/alert'; +import { file } from '../../helpers'; + +describe('IDE store alert getters', () => { + let localState; + let localStore; + + beforeEach(() => { + localStore = createStore(); + localState = localStore.state; + }); + + describe('alerts', () => { + describe('shows an alert about environments', () => { + let alert; + + beforeEach(() => { + const f = file('.gitlab-ci.yml'); + localState.openFiles.push(f); + localState.currentActivityView = 'repo-commit-section'; + localState.environmentsGuidanceAlertDetected = true; + localState.environmentsGuidanceAlertDismissed = false; + + const alertKey = getters.getAlert(localState)(f); + alert = getAlert(alertKey); + }); + + it('has a message suggesting to use environments', () => { + expect(alert.message).toEqual(EnvironmentsMessage); + }); + + it('dispatches to dismiss the callout on dismiss', () => { + jest.spyOn(localStore, 'dispatch').mockImplementation(); + alert.dismiss(localStore); + expect(localStore.dispatch).toHaveBeenCalledWith('dismissEnvironmentsGuidance'); + }); + + it('should be a tip alert', () => { + expect(alert.props).toEqual({ variant: 'tip' }); + }); + }); + }); +}); diff --git a/spec/frontend/ide/stores/mutations/alert_spec.js b/spec/frontend/ide/stores/mutations/alert_spec.js new file mode 100644 index 00000000000..2840ec4ebb7 --- /dev/null +++ b/spec/frontend/ide/stores/mutations/alert_spec.js @@ -0,0 +1,26 @@ +import * as types from '~/ide/stores/mutation_types'; +import mutations from '~/ide/stores/mutations/alert'; + +describe('~/ide/stores/mutations/alert', () => { + const state = {}; + + describe(types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, () => { + it('checks the stages for any that configure environments', () => { + mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, { + nodes: [{ groups: { nodes: [{ jobs: { nodes: [{}] } }] } }], + }); + expect(state.environmentsGuidanceAlertDetected).toBe(true); + mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, { + nodes: [{ groups: { nodes: [{ jobs: { nodes: [{ environment: {} }] } }] } }], + }); + expect(state.environmentsGuidanceAlertDetected).toBe(false); + }); + }); + + describe(types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, () => { + it('stops environments guidance', () => { + mutations[types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state); + expect(state.environmentsGuidanceAlertDismissed).toBe(true); + }); + }); +}); diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 963d5953d4c..d34358e49c0 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -45,5 +45,35 @@ RSpec.describe IdeHelper do ) end end + + context 'environments guidance experiment', :experiment do + before do + stub_experiments(in_product_guidance_environments_webide: :candidate) + self.instance_variable_set(:@project, project) + end + + context 'when project has no enviornments' do + it 'enables environment guidance' do + expect(helper.ide_data).to include('enable-environments-guidance' => 'true') + end + + context 'and the callout has been dismissed' do + it 'disables environment guidance' do + callout = create(:user_callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator) + callout.update!(dismissed_at: Time.now - 1.week) + allow(helper).to receive(:current_user).and_return(User.find(project.creator.id)) + expect(helper.ide_data).to include('enable-environments-guidance' => 'false') + end + end + end + + context 'when the project has environments' do + it 'disables environment guidance' do + create(:environment, project: project) + + expect(helper.ide_data).to include('enable-environments-guidance' => 'false') + end + end + end end end diff --git a/spec/lib/gitlab/spamcheck/client_spec.rb b/spec/lib/gitlab/spamcheck/client_spec.rb index 3384c079ffc..491e5e9a662 100644 --- a/spec/lib/gitlab/spamcheck/client_spec.rb +++ b/spec/lib/gitlab/spamcheck/client_spec.rb @@ -9,12 +9,20 @@ RSpec.describe Gitlab::Spamcheck::Client do let_it_be(:user) { create(:user, organization: 'GitLab') } let(:verdict_value) { nil } let(:error_value) { "" } + + let(:attribs_value) do + extra_attributes = Google::Protobuf::Map.new(:string, :string) + extra_attributes["monitorMode"] = "false" + extra_attributes + end + let_it_be(:issue) { create(:issue, description: 'Test issue description') } let(:response) do verdict = ::Spamcheck::SpamVerdict.new verdict.verdict = verdict_value verdict.error = error_value + verdict.extra_attributes = attribs_value verdict end @@ -45,7 +53,7 @@ RSpec.describe Gitlab::Spamcheck::Client do let(:verdict_value) { verdict } it "returns expected spam constant" do - expect(subject).to eq([expected, ""]) + expect(subject).to eq([expected, { "monitorMode" => "false" }, ""]) end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 7334165c41a..1460fe0e586 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -632,6 +632,16 @@ RSpec.describe Group do it { is_expected.to match_array([private_group, internal_group]) } end + describe 'with_onboarding_progress' do + subject { described_class.with_onboarding_progress } + + it 'joins onboarding_progress' do + create(:onboarding_progress, namespace: group) + + expect(subject).to eq([group]) + end + end + describe 'for_authorized_group_members' do let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) } diff --git a/spec/services/spam/spam_verdict_service_spec.rb b/spec/services/spam/spam_verdict_service_spec.rb index 1ec5e1f1331..91bff49b239 100644 --- a/spec/services/spam/spam_verdict_service_spec.rb +++ b/spec/services/spam/spam_verdict_service_spec.rb @@ -23,12 +23,17 @@ RSpec.describe Spam::SpamVerdictService do described_class.new(user: user, target: issue, request: request, options: {}) end + let(:attribs) do + extra_attributes = { "monitorMode" => "false" } + extra_attributes + end + describe '#execute' do subject { service.execute } before do allow(service).to receive(:akismet_verdict).and_return(nil) - allow(service).to receive(:spamcheck_verdict).and_return(nil) + allow(service).to receive(:spamcheck_verdict).and_return([nil, attribs]) end context 'if all services return nil' do @@ -63,7 +68,7 @@ RSpec.describe Spam::SpamVerdictService do context 'and they are supported' do before do allow(service).to receive(:akismet_verdict).and_return(DISALLOW) - allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) end it 'renders the more restrictive verdict' do @@ -74,7 +79,7 @@ RSpec.describe Spam::SpamVerdictService do context 'and one is supported' do before do allow(service).to receive(:akismet_verdict).and_return('nonsense') - allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) end it 'renders the more restrictive verdict' do @@ -85,13 +90,29 @@ RSpec.describe Spam::SpamVerdictService do context 'and none are supported' do before do allow(service).to receive(:akismet_verdict).and_return('nonsense') - allow(service).to receive(:spamcheck_verdict).and_return('rubbish') + allow(service).to receive(:spamcheck_verdict).and_return(['rubbish', attribs]) end it 'renders the more restrictive verdict' do expect(subject).to eq ALLOW end end + + context 'and attribs - monitorMode is true' do + let(:attribs) do + extra_attributes = { "monitorMode" => "true" } + extra_attributes + end + + before do + allow(service).to receive(:akismet_verdict).and_return(DISALLOW) + allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs]) + end + + it 'renders the more restrictive verdict' do + expect(subject).to eq(DISALLOW) + end + end end end @@ -170,16 +191,42 @@ RSpec.describe Spam::SpamVerdictService do let(:error) { '' } let(:verdict) { nil } + let(:attribs) do + extra_attributes = { "monitorMode" => "false" } + extra_attributes + end + before do allow(service).to receive(:spamcheck_client).and_return(spam_client) - allow(spam_client).to receive(:issue_spam?).and_return([verdict, error]) + allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error]) + end + + context 'if the result is a NOOP verdict' do + let(:verdict) { NOOP } + + it 'returns the verdict' do + expect(subject).to eq([NOOP, attribs]) + end + end + + context 'if attribs - monitorMode is true' do + let(:attribs) do + extra_attributes = { "monitorMode" => "true" } + extra_attributes + end + + let(:verdict) { ALLOW } + + it 'returns the verdict' do + expect(subject).to eq([ALLOW, attribs]) + end end context 'the result is a valid verdict' do let(:verdict) { ALLOW } it 'returns the verdict' do - expect(subject).to eq ALLOW + expect(subject).to eq([ALLOW, attribs]) end end @@ -203,7 +250,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { verdict_value } it "returns expected spam constant" do - expect(subject).to eq(expected) + expect(subject).to eq([expected, attribs]) end end end @@ -218,7 +265,7 @@ RSpec.describe Spam::SpamVerdictService do ::Spam::SpamConstants::DISALLOW, ::Spam::SpamConstants::BLOCK_USER].each do |verdict_value| let(:verdict) { verdict_value } - let(:expected) { verdict_value } + let(:expected) { [verdict_value, attribs] } it "returns expected spam constant" do expect(subject).to eq(expected) @@ -230,7 +277,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { :this_is_fine } it 'returns the string' do - expect(subject).to eq verdict + expect(subject).to eq([verdict, attribs]) end end @@ -238,7 +285,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { '' } it 'returns nil' do - expect(subject).to eq verdict + expect(subject).to eq([verdict, attribs]) end end @@ -246,7 +293,7 @@ RSpec.describe Spam::SpamVerdictService do let(:verdict) { nil } it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([nil, attribs]) end end @@ -254,17 +301,19 @@ RSpec.describe Spam::SpamVerdictService do let(:error) { "Sorry Dave, I can't do that" } it 'returns nil' do - expect(subject).to be_nil + expect(subject).to eq([nil, attribs]) end end context 'the requested is aborted' do + let(:attribs) { nil } + before do allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted) end it 'returns nil' do - expect(subject).to be(ALLOW) + expect(subject).to eq([ALLOW, attribs]) end end @@ -273,18 +322,20 @@ RSpec.describe Spam::SpamVerdictService do let(:error) { 'oh noes!' } it 'renders the verdict' do - expect(subject).to eq DISALLOW + expect(subject).to eq [DISALLOW, attribs] end end end context 'if the endpoint times out' do + let(:attribs) { nil } + before do allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded) end it 'returns nil' do - expect(subject).to be(ALLOW) + expect(subject).to eq([ALLOW, attribs]) end end end