diff options
91 files changed, 1906 insertions, 618 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 9fe9a8c307c..ea5a7f3a7e5 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -519,7 +519,7 @@ ee:registry-object-storage-tls: # ========================================== # Post test stage # ========================================== -allure-report: +e2e-test-report: extends: - .generate-allure-report-base - .rules:report:allure-report @@ -530,6 +530,11 @@ allure-report: ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID ALLURE_JOB_NAME: e2e-package-and-test GIT_STRATEGY: none + artifacts: # save rspec results for displaying in parent pipeline + expire_in: 1 day + when: always + paths: + - gitlab-qa-run-*/**/rspec-*.xml upload-knapsack-report: extends: diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 6134b201c23..c5a182b055a 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -81,3 +81,25 @@ e2e:package-and-test: include: - artifact: package-and-test-pipeline.yml job: e2e-test-pipeline-generate + +# Fetch child pipeline test results and store in parent pipeline +# workaround until natively implemented: https://gitlab.com/groups/gitlab-org/-/epics/8205 +e2e:package-and-test-results: + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3 + extends: + - .qa-job-base + - .qa:rules:package-and-test + stage: qa + needs: + - e2e:package-and-test + variables: + COLORIZED_LOGS: "true" + QA_LOG_LEVEL: "debug" + when: always + allow_failure: true + script: + - bundle exec rake "ci:download_test_results[e2e:package-and-test,e2e-test-report,${CI_PROJECT_DIR}]" + artifacts: + when: always + reports: + junit: gitlab-qa-run-*/**/rspec-*.xml diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index ee094a131f7..95bbbffcfbf 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -36,7 +36,7 @@ include: variables: GIT_LFS_SKIP_SMUDGE: 1 WD_INSTALL_DIR: /usr/local/bin - RSPEC_REPORT_OPTS: --force-color --order random --format documentation --format RspecJunitFormatter --out tmp/rspec.xml + RSPEC_REPORT_OPTS: --force-color --order random --format documentation --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml script: - export EE_LICENSE="$(cat $REVIEW_APPS_EE_LICENSE_FILE)" - QA_COMMAND="bundle exec bin/qa ${QA_SCENARIO} ${QA_GITLAB_URL} -- ${QA_TESTS} ${RSPEC_REPORT_OPTS}" @@ -50,7 +50,7 @@ include: paths: - qa/tmp reports: - junit: qa/tmp/rspec.xml + junit: qa/tmp/rspec-*.xml expire_in: 7 days when: always @@ -145,6 +145,11 @@ e2e-test-report: GIT_STRATEGY: none allow_failure: true when: always + artifacts: # re-save rspec results for displaying in parent pipeline + expire_in: 1 day + when: always + paths: + - qa/tmp/rspec-*.xml upload-knapsack-report: extends: diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 967f8c82158..46e62829394 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -45,6 +45,30 @@ start-review-app-pipeline: - artifact: review-app-pipeline.yml job: e2e-test-pipeline-generate +# Fetch child pipeline test results and store in parent pipeline +# workaround until natively implemented: https://gitlab.com/groups/gitlab-org/-/epics/8205 +review-app-test-results: + image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-ruby-${RUBY_VERSION}:bundler-2.3 + stage: review + extends: + - .qa-cache + - .review:rules:start-review-app-pipeline + needs: + - start-review-app-pipeline + variables: + COLORIZED_LOGS: "true" + QA_LOG_LEVEL: "debug" + before_script: + - cd qa && bundle install + script: + - bundle exec rake "ci:download_test_results[start-review-app-pipeline,e2e-test-report,${CI_PROJECT_DIR}]" + when: always + allow_failure: true + artifacts: + when: always + reports: + junit: qa/tmp/rspec-*.xml + danger-review: extends: - .default-retry diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 6181aefae12..eeacff51454 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1496,6 +1496,12 @@ changes: ["vendor/gems/mail-smtp_pool/**/*"] - <<: *if-merge-request-labels-run-all-rspec +.vendor:rules:microsoft_graph_mailer: + rules: + - <<: *if-merge-request + changes: ["vendor/gems/microsoft_graph_mailer/**/*"] + - <<: *if-merge-request-labels-run-all-rspec + .vendor:rules:ipynbdiff: rules: - <<: *if-merge-request diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml index de314df298f..577bd37ca9e 100644 --- a/.gitlab/ci/vendored-gems.gitlab-ci.yml +++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml @@ -6,6 +6,14 @@ vendor mail-smtp_pool: include: vendor/gems/mail-smtp_pool/.gitlab-ci.yml strategy: depend +vendor microsoft_graph_mailer: + extends: + - .vendor:rules:microsoft_graph_mailer + needs: [] + trigger: + include: vendor/gems/microsoft_graph_mailer/.gitlab-ci.yml + strategy: depend + vendor ipynbdiff: extends: - .vendor:rules:ipynbdiff diff --git a/.rubocop_todo/style/bare_percent_literals.yml b/.rubocop_todo/style/bare_percent_literals.yml index 104ead817d5..1a155e3cca0 100644 --- a/.rubocop_todo/style/bare_percent_literals.yml +++ b/.rubocop_todo/style/bare_percent_literals.yml @@ -1,16 +1,13 @@ --- # Cop supports --auto-correct. Style/BarePercentLiterals: - # Offense count: 220 - # Temporarily disabled due to too many offenses - Enabled: false + Details: grace period Exclude: - 'app/models/commit.rb' - 'app/models/concerns/storage/legacy_namespace.rb' - 'app/models/integrations/datadog.rb' - 'app/services/feature_flags/base_service.rb' - 'app/services/repositories/base_service.rb' - - 'app/services/repositories/destroy_service.rb' - 'ee/app/services/jira/jql_builder_service.rb' - 'ee/lib/ee/gitlab/checks/push_rules/file_size_check.rb' - 'ee/spec/features/projects/environments/environments_spec.rb' @@ -41,17 +38,15 @@ Style/BarePercentLiterals: - 'qa/qa/ee/page/project/show.rb' - 'qa/qa/ee/page/project/snippet/index.rb' - 'qa/qa/ee/page/project/wiki/show.rb' - - 'qa/qa/page/component/design_management.rb' - 'qa/qa/page/component/select2.rb' - 'qa/qa/page/element.rb' - 'qa/qa/page/file/form.rb' - 'qa/qa/page/project/web_ide/edit.rb' - 'qa/qa/resource/events/project.rb' - - 'qa/qa/resource/members.rb' + - 'qa/qa/resource/personal_access_token_cache.rb' - 'qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb' - - 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_new_account_spec.rb' + - 'qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb' - 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb' - - 'qa/qa/support/page/logging.rb' - 'qa/spec/runtime/feature_spec.rb' - 'scripts/regenerate-schema' - 'scripts/trigger-build.rb' @@ -79,6 +74,7 @@ Style/BarePercentLiterals: - 'spec/lib/banzai/filter/references/label_reference_filter_spec.rb' - 'spec/lib/banzai/filter/references/milestone_reference_filter_spec.rb' - 'spec/lib/banzai/pipeline/full_pipeline_spec.rb' + - 'spec/lib/banzai/pipeline/incident_management/timeline_event_pipeline_spec.rb' - 'spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb' - 'spec/lib/banzai/reference_parser/commit_parser_spec.rb' - 'spec/lib/banzai/reference_parser/issue_parser_spec.rb' @@ -95,6 +91,7 @@ Style/BarePercentLiterals: - 'spec/mailers/emails/releases_spec.rb' - 'spec/mailers/emails/service_desk_spec.rb' - 'spec/models/deployment_spec.rb' + - 'spec/models/incident_management/timeline_event_spec.rb' - 'spec/models/integrations/drone_ci_spec.rb' - 'spec/models/integrations/teamcity_spec.rb' - 'spec/models/project_label_spec.rb' @@ -102,6 +99,8 @@ Style/BarePercentLiterals: - 'spec/requests/api/ci/job_artifacts_spec.rb' - 'spec/requests/api/deployments_spec.rb' - 'spec/requests/api/graphql/mutations/snippets/destroy_spec.rb' + - 'spec/requests/api/graphql/project/incident_management/timeline_events_spec.rb' + - 'spec/requests/projects/packages/package_files_controller_spec.rb' - 'spec/rubocop/cop/gitlab/mark_used_feature_flags_spec.rb' - 'spec/services/prometheus/proxy_variable_substitution_service_spec.rb' - 'spec/support/banzai/reference_filter_shared_examples.rb' @@ -527,6 +527,8 @@ gem 'erubi', '~> 1.9.0' gem 'mail', '= 2.7.1' gem 'mail-smtp_pool', '~> 0.1.0', path: 'vendor/gems/mail-smtp_pool', require: false +gem 'microsoft_graph_mailer', '~> 0.1.0', path: 'vendor/gems/microsoft_graph_mailer' + # File encryption gem 'lockbox', '~> 0.6.2' diff --git a/Gemfile.lock b/Gemfile.lock index 2ec930c3296..e98c21a7fb5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,6 +31,13 @@ PATH mail (~> 2.7) PATH + remote: vendor/gems/microsoft_graph_mailer + specs: + microsoft_graph_mailer (0.1.0) + mail (~> 2.7) + oauth2 (>= 1.4.4, < 3) + +PATH remote: vendor/gems/omniauth-azure-oauth2 specs: omniauth-azure-oauth2 (0.0.10) @@ -1670,6 +1677,7 @@ DEPENDENCIES mail-smtp_pool (~> 0.1.0)! marginalia (~> 1.10.0) memory_profiler (~> 0.9) + microsoft_graph_mailer (~> 0.1.0)! mini_magick (~> 4.10.1) minitest (~> 5.11.0) multi_json (~> 1.14.1) diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue index facfc553053..b1a2b2a72ea 100644 --- a/app/assets/javascripts/diffs/components/commit_widget.vue +++ b/app/assets/javascripts/diffs/components/commit_widget.vue @@ -22,7 +22,7 @@ export default { <template> <div class="info-well mw-100 mx-0"> <div class="well-segment"> - <ul class="gl-list-style-none gl-m-0 gl-p-0"> + <ul class="blob-commit-info"> <commit-item :commit="commit" :collapsible="collapsible" /> </ul> </div> diff --git a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue deleted file mode 100644 index 7eb7fe2fadb..00000000000 --- a/app/assets/javascripts/runner/components/cells/runner_summary_cell.vue +++ /dev/null @@ -1,75 +0,0 @@ -<script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; - -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import RunnerName from '../runner_name.vue'; -import RunnerTags from '../runner_tags.vue'; -import RunnerTypeBadge from '../runner_type_badge.vue'; - -import { I18N_LOCKED_RUNNER_DESCRIPTION } from '../../constants'; - -export default { - components: { - GlIcon, - TooltipOnTruncate, - RunnerName, - RunnerTags, - RunnerTypeBadge, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - runner: { - type: Object, - required: true, - }, - }, - computed: { - runnerType() { - return this.runner.runnerType; - }, - locked() { - return this.runner.locked; - }, - description() { - return this.runner.description; - }, - ipAddress() { - return this.runner.ipAddress; - }, - }, - i18n: { - I18N_LOCKED_RUNNER_DESCRIPTION, - }, -}; -</script> - -<template> - <div> - <slot :runner="runner" name="runner-name"> - <runner-name :runner="runner" /> - </slot> - <gl-icon - v-if="locked" - v-gl-tooltip - class="gl-ml-2" - :title="$options.i18n.I18N_LOCKED_RUNNER_DESCRIPTION" - name="lock" - /> - <runner-type-badge class="gl-ml-2 gl-vertical-align-middle" :type="runnerType" size="sm" /> - - <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="description"> - {{ description }} - </tooltip-on-truncate> - <tooltip-on-truncate - v-if="ipAddress" - class="gl-display-block gl-text-truncate" - :title="ipAddress" - > - <span class="gl-md-display-none gl-lg-display-inline">{{ __('IP Address') }}</span> - <strong>{{ ipAddress }}</strong> - </tooltip-on-truncate> - <runner-tags class="gl-display-block gl-mt-2" :tag-list="runner.tagList" size="sm" /> - </div> -</template> diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue index 534317d6a57..26f1f3ce08c 100644 --- a/app/assets/javascripts/runner/components/runner_list.vue +++ b/app/assets/javascripts/runner/components/runner_list.vue @@ -1,28 +1,15 @@ <script> import { GlFormCheckbox, GlTableLite, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { __, s__ } from '~/locale'; -import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import { s__ } from '~/locale'; import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql'; import { formatJobCount, tableField } from '../utils'; -import RunnerSummaryCell from './cells/runner_summary_cell.vue'; import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue'; import RunnerStatusPopover from './runner_status_popover.vue'; import RunnerStatusCell from './cells/runner_status_cell.vue'; const defaultFields = [ tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }), - tableField({ key: 'summary', label: s__('Runners|Runner'), thClasses: ['gl-lg-w-40p'] }), - tableField({ key: 'version', label: __('Version') }), - tableField({ key: 'jobCount', label: __('Jobs') }), - tableField({ key: 'contactedAt', label: __('Last contact') }), - tableField({ key: 'actions', label: '' }), -]; - -const stackedLayoutFields = [ - tableField({ key: 'status', label: s__('Runners|Status'), thClasses: ['gl-w-15p'] }), tableField({ key: 'summary', label: s__('Runners|Runner') }), tableField({ key: 'actions', label: '', thClasses: ['gl-w-15p'] }), ]; @@ -32,17 +19,13 @@ export default { GlFormCheckbox, GlTableLite, GlSkeletonLoader, - TooltipOnTruncate, - TimeAgo, RunnerStatusPopover, - RunnerSummaryCell, RunnerStackedSummaryCell, RunnerStatusCell, }, directives: { GlTooltip: GlTooltipDirective, }, - mixins: [glFeatureFlagMixin()], apollo: { checkedRunnerIds: { query: checkedRunnerIdsQuery, @@ -72,11 +55,6 @@ export default { return { checkedRunnerIds: [] }; }, computed: { - stackedLayout() { - // runner_list_stacked_layout_admin or runner_list_stacked_layout - const { runnerListStackedLayoutAdmin, runnerListStackedLayout } = this.glFeatures || {}; - return runnerListStackedLayoutAdmin || runnerListStackedLayout; - }, tableClass() { // <gl-table-lite> does not provide a busy state, add // simple support for it. @@ -86,7 +64,7 @@ export default { }; }, fields() { - const fields = this.stackedLayout ? stackedLayoutFields : defaultFields; + const fields = defaultFields; if (this.checkable) { const checkboxField = tableField({ @@ -155,32 +133,11 @@ export default { </template> <template #cell(summary)="{ item, index }"> - <runner-stacked-summary-cell v-if="stackedLayout" :runner="item"> + <runner-stacked-summary-cell :runner="item"> <template #runner-name="{ runner }"> <slot name="runner-name" :runner="runner" :index="index"></slot> </template> </runner-stacked-summary-cell> - - <runner-summary-cell v-else :runner="item"> - <template #runner-name="{ runner }"> - <slot name="runner-name" :runner="runner" :index="index"></slot> - </template> - </runner-summary-cell> - </template> - - <template v-if="!stackedLayout" #cell(version)="{ item: { version } }"> - <tooltip-on-truncate class="gl-display-block gl-text-truncate" :title="version"> - {{ version }} - </tooltip-on-truncate> - </template> - - <template v-if="!stackedLayout" #cell(jobCount)="{ item: { jobCount } }"> - <span data-testid="job-count">{{ formatJobCount(jobCount) }}</span> - </template> - - <template v-if="!stackedLayout" #cell(contactedAt)="{ item: { contactedAt } }"> - <time-ago v-if="contactedAt" :time="contactedAt" /> - <template v-else>{{ __('Never') }}</template> </template> <template #cell(actions)="{ item }"> diff --git a/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue b/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue index c36efa73e0e..e3a9a9fd8a4 100644 --- a/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue +++ b/app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue @@ -4,7 +4,6 @@ import { GlBanner } from '@gitlab/ui'; import { s__ } from '~/locale'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const I18N_TITLE = s__("Runners|We've made some changes and want your feedback"); const I18N_DESCRIPTION = s__( @@ -22,23 +21,11 @@ export default { GlBanner, LocalStorageSync, }, - mixins: [glFeatureFlagMixin()], data() { return { isDismissed: false, }; }, - computed: { - stackedLayoutEnabled() { - // Two feature flags can be used: runner_list_stacked_layout_admin or runner_list_stacked_layout - // Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/371031 - const { runnerListStackedLayoutAdmin, runnerListStackedLayout } = this.glFeatures || {}; - return runnerListStackedLayoutAdmin || runnerListStackedLayout; - }, - showBanner() { - return this.stackedLayoutEnabled && !this.isDismissed; - }, - }, methods: { onClose() { this.isDismissed = true; @@ -57,7 +44,7 @@ export default { <div> <local-storage-sync v-model="isDismissed" :storage-key="$options.STORAGE_KEY" /> <gl-banner - v-if="showBanner" + v-if="!isDismissed" :svg-path="$options.ILLUSTRATION_URL" :title="$options.I18N_TITLE" :button-text="$options.I18N_LINK" diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss index 15d9cbfd8c0..9e81e1d4771 100644 --- a/app/assets/stylesheets/_page_specific_files.scss +++ b/app/assets/stylesheets/_page_specific_files.scss @@ -27,4 +27,5 @@ @import './pages/service_desk'; @import './pages/settings'; @import './pages/storage_quota'; +@import './pages/tree'; @import './pages/users'; diff --git a/app/assets/stylesheets/page_bundles/graph_charts.scss b/app/assets/stylesheets/page_bundles/graph_charts.scss deleted file mode 100644 index 7b60cbe3043..00000000000 --- a/app/assets/stylesheets/page_bundles/graph_charts.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import 'page_bundles/mixins_and_variables_and_functions'; - -.repo-charts { - .sub-header { - margin: 20px 0; - } - - .sub-header-block.border-top { - margin-top: 20px; - padding: 0; - border-top: 1px solid $white-dark; - border-bottom: 0; - } - - .commit-stats li { - font-size: 16px; - } - - .tree-ref-header { - margin-bottom: 20px; - - h4 { - margin: 0; - line-height: 36px; - } - } -} diff --git a/app/assets/stylesheets/page_bundles/tree.scss b/app/assets/stylesheets/pages/tree.scss index f79386bba15..a9fbff8958d 100644 --- a/app/assets/stylesheets/page_bundles/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,5 +1,3 @@ -@import 'mixins_and_variables_and_functions'; - .project-last-commit { min-height: 4.75rem; } @@ -210,3 +208,39 @@ margin-top: $gl-padding; } +.blob-upload-dropzone-previews { + display: flex; + justify-content: center; + align-items: center; + text-align: center; + border: 2px; + border-style: dashed; + border-color: $border-color; + min-height: 200px; +} + +.repo-charts { + .sub-header { + margin: 20px 0; + } + + .sub-header-block.border-top { + margin-top: 20px; + padding: 0; + border-top: 1px solid $white-dark; + border-bottom: 0; + } + + .commit-stats li { + font-size: 16px; + } + + .tree-ref-header { + margin-bottom: 20px; + + h4 { + margin: 0; + line-height: 36px; + } + } +} diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 42d3387d27e..a0f72f5e58c 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -6,7 +6,6 @@ class Admin::RunnersController < Admin::ApplicationController before_action :runner, except: [:index, :tag_list, :runner_setup_scripts] before_action only: [:index] do push_frontend_feature_flag(:admin_runners_bulk_delete) - push_frontend_feature_flag(:runner_list_stacked_layout_admin) end before_action only: [:show] do diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index 3feb6d48fae..652f12e34ba 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -4,9 +4,6 @@ class Groups::RunnersController < Groups::ApplicationController before_action :authorize_read_group_runners!, only: [:index, :show] before_action :authorize_admin_group_runners!, only: [:edit, :update, :destroy, :pause, :resume] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] - before_action only: [:index] do - push_frontend_feature_flag(:runner_list_stacked_layout, @group) - end before_action only: [:show] do push_frontend_feature_flag(:enforce_runner_token_expires_at) diff --git a/app/services/ci/job_artifacts/track_artifact_report_service.rb b/app/services/ci/job_artifacts/track_artifact_report_service.rb index d553d0b4e6f..1be1d98394f 100644 --- a/app/services/ci/job_artifacts/track_artifact_report_service.rb +++ b/app/services/ci/job_artifacts/track_artifact_report_service.rb @@ -6,12 +6,11 @@ module Ci include Gitlab::Utils::UsageData REPORT_TRACKED = %i[test].freeze - VALUES_DELIMITER = '_' def execute(pipeline) REPORT_TRACKED.each do |report| if pipeline.complete_and_has_reports?(Ci::JobArtifact.of_report_type(report)) - track_usage_event(event_name(report), [pipeline.id, pipeline.user_id].join(VALUES_DELIMITER)) + track_usage_event(event_name(report), pipeline.user_id) end end end diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index c91dfe6d28e..3ebac785d55 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title _('Artifacts') - page_title @path.presence, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs') -- add_page_specific_style 'page_bundles/tree' = render "projects/jobs/header" diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml index e16e3ef266d..1ad70506be4 100644 --- a/app/views/projects/artifacts/file.html.haml +++ b/app/views/projects/artifacts/file.html.haml @@ -1,5 +1,4 @@ - page_title @path, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs') -- add_page_specific_style 'page_bundles/tree' = render "projects/jobs/header" diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 2379e00aebc..d4efca668eb 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -1,5 +1,4 @@ - page_title _("Blame"), @blob.path, @ref -- add_page_specific_style 'page_bundles/tree' #blob-content-holder.tree-holder{ data: { testid: 'blob-content-holder' } } = render "projects/blob/breadcrumb", blob: @blob, blame: true diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index c8cf12c36f9..33b2229f5d1 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -1,6 +1,5 @@ - breadcrumb_title _('Repository') - page_title @blob.path, @ref -- add_page_specific_style 'page_bundles/tree' - signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1) - content_for :prefetch_asset_tags do - webpack_preload_asset_tag('monaco', prefetch: true) diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index ae68a13929e..6b06584ea25 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -1,5 +1,5 @@ - breadcrumb_title _("Commits") -- add_page_specific_style 'page_bundles/tree' + - page_title _("Commits"), @ref = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index 7cd4ab08680..2e024b8ffc4 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -1,5 +1,4 @@ - page_title _("Find File"), @ref -- add_page_specific_style 'page_bundles/tree' .file-finder-holder.tree-holder.clearfix.js-file-finder{ 'data-file-find-url': "#{escape_javascript(project_files_path(@project, @ref, format: :json))}", 'data-find-tree-url': escape_javascript(project_tree_path(@project, @ref)), 'data-blob-url-template': escape_javascript(project_blob_path(@project, @ref)) } .nav-block diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index edf8f71c673..04d400688d4 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -1,5 +1,4 @@ - page_title _("Repository Analytics") -- add_page_specific_style 'page_bundles/graph_charts' .mb-3 %h3 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index c7ac28fa194..e9d1661a4f1 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -2,7 +2,6 @@ - @content_class = "limit-container-width" unless fluid_layout - @skip_current_level_breadcrumb = true - add_page_specific_style 'page_bundles/project' -- add_page_specific_style 'page_bundles/tree' = content_for :meta_tags do = auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity") diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 6d1ab80bdc5..1553eda1cfb 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -1,4 +1,3 @@ -- add_page_specific_style 'page_bundles/tree' - current_route_path = request.fullpath.match(%r{-/tree/[^/]+/(.+$)}).to_a[1] - add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" }) - add_page_startup_graphql_call('repository/permissions', { projectPath: @project.full_path }) diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml new file mode 100644 index 00000000000..48fe258d01f --- /dev/null +++ b/app/views/shared/_commit_well.html.haml @@ -0,0 +1,4 @@ +.info-well.d-none.d-sm-block.project-last-commit.gl-mb-3 + .well-segment + %ul.blob-commit-info + = render 'projects/commits/commit', commit: commit, ref: ref, project: project diff --git a/config/application.rb b/config/application.rb index 4647d7d9558..03c8eadc4b0 100644 --- a/config/application.rb +++ b/config/application.rb @@ -269,7 +269,6 @@ module Gitlab config.assets.precompile << "page_bundles/epics.css" config.assets.precompile << "page_bundles/error_tracking_details.css" config.assets.precompile << "page_bundles/error_tracking_index.css" - config.assets.precompile << "page_bundles/graph_charts.css" config.assets.precompile << "page_bundles/group.css" config.assets.precompile << "page_bundles/ide.css" config.assets.precompile << "page_bundles/import.css" @@ -307,7 +306,6 @@ module Gitlab config.assets.precompile << "page_bundles/terminal.css" config.assets.precompile << "page_bundles/terms.css" config.assets.precompile << "page_bundles/todos.css" - config.assets.precompile << "page_bundles/tree.css" config.assets.precompile << "page_bundles/wiki.css" config.assets.precompile << "page_bundles/work_items.css" config.assets.precompile << "page_bundles/xterm.css" diff --git a/config/feature_flags/development/runner_list_stacked_layout.yml b/config/feature_flags/development/runner_list_stacked_layout.yml deleted file mode 100644 index bb5f9c8e922..00000000000 --- a/config/feature_flags/development/runner_list_stacked_layout.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: runner_list_stacked_layout -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95617 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371031 -milestone: '15.4' -type: development -group: group::runner -default_enabled: false diff --git a/config/feature_flags/development/runner_list_stacked_layout_admin.yml b/config/feature_flags/development/runner_list_stacked_layout_admin.yml deleted file mode 100644 index 4f4688cce89..00000000000 --- a/config/feature_flags/development/runner_list_stacked_layout_admin.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: runner_list_stacked_layout_admin -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95617 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371031 -milestone: '15.4' -type: development -group: group::runner -default_enabled: false diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 6861864999f..da950c54fbf 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -183,6 +183,22 @@ production: &base # plaintext. This can be a security risk. # display_initial_root_password: false + # Allows delivery of emails using Microsoft Graph API with OAuth 2.0 client credentials flow. + microsoft_graph_mailer: + enabled: false + # The unique identifier for the user. To use Microsoft Graph on behalf of the user. + # user_id: "YOUR-USER-ID" + # The directory tenant the application plans to operate against, in GUID or domain-name format. + # tenant: "YOUR-TENANT-ID" + # The application ID that's assigned to your app. You can find this information in the portal where you registered your app. + # client_id: "YOUR-CLIENT-ID" + # The client secret that you generated for your app in the app registration portal. + # client_secret: "YOUR-CLIENT-SECRET-ID" + # Defaults to "https://login.microsoftonline.com". + # azure_ad_endpoint: + # Defaults to "https://graph.microsoft.com". + # graph_endpoint: + ## Reply by email # Allow users to comment on issues and merge requests by replying to notification emails. # For documentation on how to set this up, see https://docs.gitlab.com/ee/administration/reply_by_email.html diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0dc4a9223af..c3ee7e06396 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -884,6 +884,18 @@ Settings['satellites'] ||= Settingslogic.new({}) Settings.satellites['path'] = Settings.absolute(Settings.satellites['path'] || "tmp/repo_satellites/") # +# Microsoft Graph Mailer +# +Settings['microsoft_graph_mailer'] ||= Settingslogic.new({}) +Settings.microsoft_graph_mailer['enabled'] = false if Settings.microsoft_graph_mailer['enabled'].nil? +Settings.microsoft_graph_mailer['user_id'] ||= nil +Settings.microsoft_graph_mailer['tenant'] ||= nil +Settings.microsoft_graph_mailer['client_id'] ||= nil +Settings.microsoft_graph_mailer['client_secret'] ||= nil +Settings.microsoft_graph_mailer['azure_ad_endpoint'] ||= 'https://login.microsoftonline.com' +Settings.microsoft_graph_mailer['graph_endpoint'] ||= 'https://graph.microsoft.com' + +# # Kerberos # Gitlab.ee do diff --git a/config/initializers/microsoft_graph_mailer.rb b/config/initializers/microsoft_graph_mailer.rb new file mode 100644 index 00000000000..45fdef1c57d --- /dev/null +++ b/config/initializers/microsoft_graph_mailer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +if Gitlab.config.microsoft_graph_mailer.enabled + ActionMailer::Base.delivery_method = :microsoft_graph + + ActionMailer::Base.microsoft_graph_settings = { + user_id: Gitlab.config.microsoft_graph_mailer.user_id, + tenant: Gitlab.config.microsoft_graph_mailer.tenant, + client_id: Gitlab.config.microsoft_graph_mailer.client_id, + client_secret: Gitlab.config.microsoft_graph_mailer.client_secret, + azure_ad_endpoint: Gitlab.config.microsoft_graph_mailer.azure_ad_endpoint, + graph_endpoint: Gitlab.config.microsoft_graph_mailer.graph_endpoint + } +end diff --git a/db/post_migrate/20220706145113_backfill_namespace_id_on_issues.rb b/db/post_migrate/20220706145113_backfill_namespace_id_on_issues.rb new file mode 100644 index 00000000000..8114967ac8e --- /dev/null +++ b/db/post_migrate/20220706145113_backfill_namespace_id_on_issues.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class BackfillNamespaceIdOnIssues < Gitlab::Database::Migration[2.0] + restrict_gitlab_migration gitlab_schema: :gitlab_main + disable_ddl_transaction! + + MIGRATION = 'BackfillProjectNamespaceOnIssues' + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 500 + MAX_BATCH_SIZE = 10_000 + SUB_BATCH_SIZE = 10 + + def up + queue_batched_background_migration( + MIGRATION, + :issues, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + max_batch_size: MAX_BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :issues, :id, []) + end +end diff --git a/db/schema_migrations/20220706145113 b/db/schema_migrations/20220706145113 new file mode 100644 index 00000000000..8ed19a92025 --- /dev/null +++ b/db/schema_migrations/20220706145113 @@ -0,0 +1 @@ +e37da383a2e69e5e3157180b33017fc64af6ee009fc3dd317ae69931d37c6350
\ No newline at end of file diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml index dc05aa05730..ac3e495b6f8 100644 --- a/doc/.vale/gitlab/Uppercase.yml +++ b/doc/.vale/gitlab/Uppercase.yml @@ -81,6 +81,7 @@ exceptions: - FREE - FTP - GCP + - GCS - GDK - GDPR - GET diff --git a/doc/api/resource_state_events.md b/doc/api/resource_state_events.md index b2e886618d5..8e957df8145 100644 --- a/doc/api/resource_state_events.md +++ b/doc/api/resource_state_events.md @@ -8,8 +8,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35210/) in GitLab 13.2. -Resource state events keep track of what happens to GitLab [issues](../user/project/issues/index.md) and -[merge requests](../user/project/merge_requests/index.md). +Resource state events keep track of what happens to GitLab [issues](../user/project/issues/index.md) +[merge requests](../user/project/merge_requests/index.md) and [epics starting with GitLab 15.4](../user/group/epics/index.md) Use them to track which state was set, who did it, and when it happened. @@ -212,3 +212,105 @@ Example response: "state": "closed" } ``` + +## Epics + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97554) in GitLab 15.4. + +### List group epic state events + +Returns a list of all state events for a single epic. + +```plaintext +GET /groups/:id/epics/:epic_id/resource_state_events +``` + +| Attribute | Type | Required | Description | +|-------------| -------------- | -------- |--------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding). | +| `epic_id` | integer | yes | The ID of an epic. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/resource_state_events" +``` + +Example response: + +```json +[ + { + "id": 142, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-20T13:38:20.077Z", + "resource_type": "Epic", + "resource_id": 11, + "state": "opened" + }, + { + "id": 143, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-21T14:38:20.077Z", + "resource_type": "Epic", + "resource_id": 11, + "state": "closed" + } +] +``` + +### Get single epic state event + +Returns a single state event for a specific group epic. + +```plaintext +GET /groups/:id/epics/:epic_id/resource_state_events/:resource_state_event_id +``` + +Parameters: + +| Attribute | Type | Required | Description | +|---------------------------| -------------- | -------- |-------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding). | +| `epic_id` | integer | yes | The ID of an epic. | +| `resource_state_event_id` | integer | yes | The ID of a state event. | + +Example request: + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/epics/11/resource_state_events/143" +``` + +Example response: + +```json +{ + "id": 143, + "user": { + "id": 1, + "name": "Administrator", + "username": "root", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", + "web_url": "http://gitlab.example.com/root" + }, + "created_at": "2018-08-21T14:38:20.077Z", + "resource_type": "Epic", + "resource_id": 11, + "state": "closed" +} +``` diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md index dbe113408df..812683ef2c1 100644 --- a/doc/ci/jobs/ci_job_token.md +++ b/doc/ci/jobs/ci_job_token.md @@ -24,12 +24,6 @@ You can use a GitLab CI/CD job token to authenticate with specific API endpoints - [Releases](../../api/releases/index.md) and [Release links](../../api/releases/links.md). - [Terraform plan](../../user/infrastructure/index.md). -NOTE: -There's an open issue, -[GitLab-#333444](https://gitlab.com/gitlab-org/gitlab/-/issues/333444), -which prevents you from using a job token with internal projects. This bug only impacts self-managed -GitLab instances. - The token has the same permissions to access the API as the user that caused the job to run. A user can cause a job to run by pushing a commit, triggering a manual job, being the owner of a scheduled pipeline, and so on. Therefore, this user must be assigned to diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md index b08e759a93a..63dad3070c7 100644 --- a/doc/development/feature_flags/controls.md +++ b/doc/development/feature_flags/controls.md @@ -339,7 +339,7 @@ take one of the following actions: To remove a feature flag, open **one merge request** to make the changes. In the MR: -1. Add the ~"feature flag" label so release managers are aware the changes are hidden behind a feature flag. +1. Add the ~"feature flag" label so release managers are aware of the removal. 1. If the merge request has to be picked into a stable branch, add the appropriate `~"Pick into X.Y"` label, for example `~"Pick into 13.0"`. See [the feature flag process](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#including-a-feature-behind-feature-flag-in-the-final-release) diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index 2fdbeb26ee5..e0441310523 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -514,12 +514,12 @@ You can also enable a feature flag for a given gate: Feature.enable(:feature_flag_name, Project.find_by_full_path("root/my-project")) ``` -### Removing a feature flag locally (in development) +### Disabling a feature flag locally (in development) When manually enabling or disabling a feature flag from the Rails console, its default value gets overwritten. This can cause confusion when changing the flag's `default_enabled` attribute. -To reset the feature flag to the default status, you can remove it in the rails console (`rails c`) +To reset the feature flag to the default status, you can disable it in the rails console (`rails c`) as follows: ```ruby diff --git a/doc/raketasks/backup_gitlab.md b/doc/raketasks/backup_gitlab.md index 48230b28eb7..9e3e49a60b8 100644 --- a/doc/raketasks/backup_gitlab.md +++ b/doc/raketasks/backup_gitlab.md @@ -929,3 +929,51 @@ For installations from source: 1. [Restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect. + +## Troubleshooting + +The following are solutions to problems you may encounter while uploading backups to remote storage. + +### Large backups may not upload to remote storage + +When trying to upload large uploads to remote storage (AWS or GCS), you may experience an issue where the backup isn't uploaded. + +After creating the backup archive, the `backup_json.log` log does not indicate the upload as done: + +```shell +..."Uploading backup archive to remote storage REDACTED ...done +``` + +For example: + +```json +{"severity":"INFO","time":"2022-08-19T14:11:11.111Z","correlation_id":null,"message":"Creating backup archive: XXXX.15.2.2-ee_gitlab_backup.tar ... done"} +{"severity":"INFO","time":"2022-08-19T14:38:32.134Z","correlation_id":null,"message":"Uploading backup archive to remote storage REDACTED ... "} +``` + +1. Check the rails console log for an error: + + ```shell + rake aborted! + Excon::Error::Socket: Broken pipe (Excon::Error) + /opt/gitlab/embedded/service/gitlab-rails/lib/backup/manager.rb:324:in `upload' + [...] + ``` + +1. Check the storage bucket to confirm that the backup wasn't uploaded. +1. Confirm that you don't have any storage quotas on your buckets preventing the upload. + +To increase the timeout for large backups for Omnibus GitLab packages: + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['backup_upload_connection'] = { + # [...] + 'connection_options' => { + 'write_timeout' => 3600 # Increase the upload timeout from the default 60 to 3600 + } + ``` + +1. [Reconfigure GitLab](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) +for the changes to take effect. diff --git a/lib/api/helpers/resource_events_helpers.rb b/lib/api/helpers/resource_events_helpers.rb new file mode 100644 index 00000000000..c47a58e8fce --- /dev/null +++ b/lib/api/helpers/resource_events_helpers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Helpers + module ResourceEventsHelpers + def self.eventable_types + # This is a method instead of a constant, allowing EE to more easily extend it. + { + Issue => { feature_category: :team_planning, id_field: 'IID' }, + MergeRequest => { feature_category: :code_review, id_field: 'IID' } + } + end + end + end +end + +API::Helpers::ResourceEventsHelpers.prepend_mod_with('API::Helpers::ResourceEventsHelpers') diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb deleted file mode 100644 index eeb68362c1d..00000000000 --- a/lib/api/helpers/resource_label_events_helpers.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module API - module Helpers - module ResourceLabelEventsHelpers - def self.feature_category_per_eventable_type - # This is a method instead of a constant, allowing EE to more easily - # extend it. - { - Issue => :team_planning, - MergeRequest => :code_review - } - end - end - end -end - -API::Helpers::ResourceLabelEventsHelpers.prepend_mod_with('API::Helpers::ResourceLabelEventsHelpers') diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index cd56809f45a..e74b6509a17 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -7,20 +7,22 @@ module API before { authenticate! } - Helpers::ResourceLabelEventsHelpers.feature_category_per_eventable_type.each do |eventable_type, feature_category| + Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize + human_eventable_str = eventable_type.to_s.underscore.humanize.downcase + feature_category = details[:feature_category] params do requires :id, type: String, desc: "The ID of a #{parent_type}" end resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do + desc "Get a list of #{human_eventable_str} resource label events" do success Entities::ResourceLabelEvent detail 'This feature was introduced in 11.3' end params do - requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}" use :pagination end @@ -32,13 +34,13 @@ module API present ResourceLabelEvent.visible_to_user?(current_user, paginate(events)), with: Entities::ResourceLabelEvent end - desc "Get a single #{eventable_type.to_s.downcase} resource label event" do + desc "Get a single #{human_eventable_str} resource label event" do success Entities::ResourceLabelEvent detail 'This feature was introduced in 11.3' end params do requires :event_id, type: String, desc: 'The ID of a resource label event' - requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}" end get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb index 4b92f320d6f..f817d55c505 100644 --- a/lib/api/resource_state_events.rb +++ b/lib/api/resource_state_events.rb @@ -7,41 +7,41 @@ module API before { authenticate! } - { - Issue => :team_planning, - MergeRequest => :code_review - }.each do |eventable_class, feature_category| - eventable_name = eventable_class.to_s.underscore + Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details| + parent_type = eventable_type.parent_class.to_s.underscore + eventables_str = eventable_type.to_s.underscore.pluralize + human_eventable_str = eventable_type.to_s.underscore.humanize.downcase + feature_category = details[:feature_category] params do - requires :id, type: String, desc: "The ID of a project" + requires :id, type: String, desc: "The ID of a #{parent_type}" end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "Get a list of #{eventable_class.to_s.downcase} resource state events" do + resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc "Get a list of #{human_eventable_str} resource state events" do success Entities::ResourceStateEvent end params do - requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}" + requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}" use :pagination end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category, urgency: :low do - eventable = find_noteable(eventable_class, params[:eventable_iid]) + get ":id/#{eventables_str}/:eventable_id/resource_state_events", feature_category: feature_category, urgency: :low do + eventable = find_noteable(eventable_type, params[:eventable_id]) events = ResourceStateEventFinder.new(current_user, eventable).execute present paginate(events), with: Entities::ResourceStateEvent end - desc "Get a single #{eventable_class.to_s.downcase} resource state event" do + desc "Get a single #{human_eventable_str} resource state event" do success Entities::ResourceStateEvent end params do - requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}" + requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}" requires :event_id, type: Integer, desc: 'The ID of a resource state event' end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id", feature_category: feature_category do - eventable = find_noteable(eventable_class, params[:eventable_iid]) + get ":id/#{eventables_str}/:eventable_id/resource_state_events/:event_id", feature_category: feature_category do + eventable = find_noteable(eventable_type, params[:eventable_id]) event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id]) diff --git a/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb b/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb new file mode 100644 index 00000000000..815c346bb39 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Back-fills the `issues.namespace_id` by setting it to corresponding project.project_namespace_id + class BackfillProjectNamespaceOnIssues < BatchedMigrationJob + def perform + each_sub_batch( + operation_name: :update_all, + batching_scope: -> (relation) { + relation.joins("INNER JOIN projects ON projects.id = issues.project_id") + .select("issues.id AS issue_id, projects.project_namespace_id").where(issues: { namespace_id: nil }) + } + ) do |sub_batch| + connection.execute <<~SQL + UPDATE issues + SET namespace_id = projects.project_namespace_id + FROM (#{sub_batch.to_sql}) AS projects(issue_id, project_namespace_id) + WHERE issues.id = issue_id + SQL + end + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index a68218f84b7..f95209493ba 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -701,7 +701,9 @@ module Gitlab # Delete the specified branch from the repository # Note: No Git hooks are executed for this action def delete_branch(branch_name) - write_ref(branch_name, Gitlab::Git::BLANK_SHA) + branch_name = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" unless branch_name.start_with?("refs/") + + delete_refs(branch_name) rescue CommandError => e raise DeleteBranchError, e end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 9a08aedd78c..8fa6d3f23dc 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -9,6 +9,7 @@ module QA HTTP_STATUS_CREATED = 201 HTTP_STATUS_NO_CONTENT = 204 HTTP_STATUS_ACCEPTED = 202 + HTTP_STATUS_PERMANENT_REDIRECT = 308 HTTP_STATUS_NOT_FOUND = 404 HTTP_STATUS_TOO_MANY_REQUESTS = 429 HTTP_STATUS_SERVER_ERROR = 500 diff --git a/qa/qa/tools/ci/helpers.rb b/qa/qa/tools/ci/helpers.rb index 69625ad689b..55bb123de20 100644 --- a/qa/qa/tools/ci/helpers.rb +++ b/qa/qa/tools/ci/helpers.rb @@ -3,12 +3,46 @@ module QA module Tools module Ci + # Helpers for CI related tasks + # module Helpers + include Support::API + # Logger instance # # @return [Logger] def logger - @logger ||= Gitlab::QA::TestLogger.logger(level: "INFO", source: "CI Tools") + @logger ||= Gitlab::QA::TestLogger.logger( + level: Gitlab::QA::Runtime::Env.log_level, + source: "CI Tools" + ) + end + + # Api get request + # + # @param [String] path + # @param [Hash] args + # @return [Hash, Array] + def api_get(path, **args) + response = get("#{api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => access_token }, **args }) + response = response.follow_redirection if response.code == Support::API::HTTP_STATUS_PERMANENT_REDIRECT + raise "Request failed: '#{response.body}'" unless response.code == Support::API::HTTP_STATUS_OK + + args[:raw_response] ? response : parse_body(response) + end + + # Gitlab api url + # + # @return [String] + def api_url + @api_url ||= ENV.fetch('CI_API_V4_URL', 'https://gitlab.com/api/v4') + end + + # Api access token + # + # @return [String] + def access_token + @access_token ||= ENV.fetch('QA_GITLAB_CI_TOKEN') { raise('Variable QA_GITLAB_CI_TOKEN missing') } end end end diff --git a/qa/qa/tools/ci/test_results.rb b/qa/qa/tools/ci/test_results.rb new file mode 100644 index 00000000000..635b69f6ca0 --- /dev/null +++ b/qa/qa/tools/ci/test_results.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module QA + module Tools + module Ci + class TestResults + include Helpers + + def initialize(pipeline_name, test_report_job_name, report_path) + @pipeline_name = pipeline_name + @test_report_job_name = test_report_job_name + @report_path = report_path + end + + # Get test report artifacts from downstream pipeline + # + # @param [String] pipeline_name + # @param [String] test_report_job_name + # @param [String] report_path + # @return [void] + def self.get(pipeline_name, test_report_job_name, report_path) + new(pipeline_name, test_report_job_name, report_path).download_test_results + end + + # Download test results from child pipeline + # + # @return [void] + def download_test_results + logger.info("Fetching test results for '#{pipeline_name}'") + + logger.debug(" fetching pipeline id of '#{pipeline_name}' child pipeline") + downstream_pipeline_id = api_get("#{pipelines_url(pipeline_id)}/bridges") + .find { |bridge| bridge[:name] == pipeline_name } + &.dig(:downstream_pipeline, :id) + return logger.error("Child pipeline '#{pipeline_name}' not found!") unless downstream_pipeline_id + + logger.debug(" fetching job id of test report job") + job_id = api_get("#{pipelines_url(downstream_pipeline_id)}/jobs") + .find { |job| job[:name] == test_report_job_name } + &.fetch(:id) + return logger.error("Test report job '#{test_report_job_name}' not found!") unless job_id + + logger.debug(" fetching test results artifact archive") + response = api_get("/projects/#{project_id}/jobs/#{job_id}/artifacts", raw_response: true) + + logger.info("Extracting test result archive") + system("unzip", "-o", "-d", report_path, response.file.path) + end + + private + + attr_reader :pipeline_name, :test_report_job_name, :report_path + + # Base get pipeline url + # + # @param [Integer] id + # @return [String] + def pipelines_url(id) + "/projects/#{project_id}/pipelines/#{id}" + end + + # Current pipeline id + # + # @return [String] + def pipeline_id + ENV["CI_PIPELINE_ID"] + end + + # Current project id + # + # @return [String] + def project_id + ENV["CI_PROJECT_ID"] + end + end + end + end +end diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake index b59bd773149..44a794d9f94 100644 --- a/qa/tasks/ci.rake +++ b/qa/tasks/ci.rake @@ -52,5 +52,10 @@ namespace :ci do QA_FEATURE_FLAGS='#{feature_flags}' TXT end + + desc "Download test results from downstream pipeline" + task :download_test_results, [:trigger_name, :test_report_job_name, :report_path] do |_, args| + QA::Tools::Ci::TestResults.get(args[:trigger_name], args[:test_report_job_name], args[:report_path]) + end end # rubocop:enable Rails/RakeEnvironment diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb index 46fd524adbb..9b721d8cfca 100644 --- a/spec/config/settings_spec.rb +++ b/spec/config/settings_spec.rb @@ -164,4 +164,16 @@ RSpec.describe Settings do end end end + + describe '.microsoft_graph_mailer' do + it 'defaults' do + expect(described_class.microsoft_graph_mailer.enabled).to be false + expect(described_class.microsoft_graph_mailer.user_id).to be_nil + expect(described_class.microsoft_graph_mailer.tenant).to be_nil + expect(described_class.microsoft_graph_mailer.client_id).to be_nil + expect(described_class.microsoft_graph_mailer.client_secret).to be_nil + expect(described_class.microsoft_graph_mailer.azure_ad_endpoint).to eq('https://login.microsoftonline.com') + expect(described_class.microsoft_graph_mailer.graph_endpoint).to eq('https://graph.microsoft.com') + end + end end diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 83c11f5d356..5133c02b190 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -29,7 +29,6 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; import IssuesListApp from '~/issues/list/components/issues_list_app.vue'; import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue'; - import { CREATED_DESC, RELATIVE_POSITION, @@ -58,9 +57,11 @@ import { WORK_ITEM_TYPE_ENUM_TASK, WORK_ITEM_TYPE_ENUM_TEST_CASE, } from '~/work_items/constants'; - import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import('~/issuable/bulk_update_sidebar'); +import('~/users_select'); + jest.mock('@sentry/browser'); jest.mock('~/flash'); jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() })); diff --git a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/runner/components/cells/runner_summary_cell_spec.js deleted file mode 100644 index f17d66c7ef4..00000000000 --- a/spec/frontend/runner/components/cells/runner_summary_cell_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { __ } from '~/locale'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import RunnerSummaryCell from '~/runner/components/cells/runner_summary_cell.vue'; -import RunnerTags from '~/runner/components/runner_tags.vue'; -import { INSTANCE_TYPE, I18N_INSTANCE_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -const mockId = '1'; -const mockShortSha = '2P6oDVDm'; -const mockDescription = 'runner-1'; -const mockIpAddress = '0.0.0.0'; -const mockTagList = ['shell', 'linux']; - -describe('RunnerTypeCell', () => { - let wrapper; - - const findLockIcon = () => wrapper.findByTestId('lock-icon'); - const findRunnerTags = () => wrapper.findComponent(RunnerTags); - - const createComponent = (runner, options) => { - wrapper = mountExtended(RunnerSummaryCell, { - propsData: { - runner: { - id: `gid://gitlab/Ci::Runner/${mockId}`, - shortSha: mockShortSha, - description: mockDescription, - ipAddress: mockIpAddress, - runnerType: INSTANCE_TYPE, - tagList: mockTagList, - ...runner, - }, - }, - ...options, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('Displays the runner name as id and short token', () => { - expect(wrapper.text()).toContain(`#${mockId} (${mockShortSha})`); - }); - - it('Displays the runner type', () => { - expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE); - }); - - it('Does not display the locked icon', () => { - expect(findLockIcon().exists()).toBe(false); - }); - - it('Displays the locked icon for locked runners', () => { - createComponent({ - runnerType: PROJECT_TYPE, - locked: true, - }); - - expect(findLockIcon().exists()).toBe(true); - }); - - it('Displays the runner description', () => { - expect(wrapper.text()).toContain(mockDescription); - }); - - it('Displays ip address', () => { - expect(wrapper.text()).toContain(`${__('IP Address')} ${mockIpAddress}`); - }); - - it('Displays no ip address', () => { - createComponent({ - ipAddress: null, - }); - - expect(wrapper.text()).not.toContain(__('IP Address')); - }); - - it('Displays tag list', () => { - expect(findRunnerTags().props('tagList')).toEqual(mockTagList); - }); - - it('Displays a custom slot', () => { - const slotContent = 'My custom runner summary'; - - createComponent( - {}, - { - slots: { - 'runner-name': slotContent, - }, - }, - ); - - expect(wrapper.text()).toContain(slotContent); - }); -}); diff --git a/spec/frontend/runner/components/runner_list_spec.js b/spec/frontend/runner/components/runner_list_spec.js index 1e434ae683f..54a9e713721 100644 --- a/spec/frontend/runner/components/runner_list_spec.js +++ b/spec/frontend/runner/components/runner_list_spec.js @@ -65,9 +65,6 @@ describe('RunnerList', () => { expect(headerLabels).toEqual([ 'Status', 'Runner', - 'Version', - 'Jobs', - 'Last contact', '', // actions has no label ]); }); @@ -87,23 +84,28 @@ describe('RunnerList', () => { }); it('Displays details of a runner', () => { - const { id, description, version, shortSha } = mockRunners[0]; - createComponent({}, mountExtended); + const { id, description, version, shortSha } = mockRunners[0]; + const numericId = getIdFromGraphQLId(id); + // Badges - expect(findCell({ fieldKey: 'status' }).text()).toBe(I18N_STATUS_NEVER_CONTACTED); + expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( + I18N_STATUS_NEVER_CONTACTED, + ); // Runner summary - expect(findCell({ fieldKey: 'summary' }).text()).toContain( - `#${getIdFromGraphQLId(id)} (${shortSha})`, - ); - expect(findCell({ fieldKey: 'summary' }).text()).toContain(description); + const summary = findCell({ fieldKey: 'summary' }).text(); - // Other fields - expect(findCell({ fieldKey: 'version' }).text()).toBe(version); - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('0'); - expect(findCell({ fieldKey: 'contactedAt' }).text()).toEqual(expect.any(String)); + expect(summary).toContain(`#${numericId} (${shortSha})`); + expect(summary).toContain(I18N_PROJECT_TYPE); + + expect(summary).toContain(version); + expect(summary).toContain(description); + + expect(summary).toContain('Last contact'); + expect(summary).toContain('0'); // job count + expect(summary).toContain('Created'); // Actions expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); @@ -162,42 +164,6 @@ describe('RunnerList', () => { }); }); - describe('Table data formatting', () => { - let mockRunnersCopy; - - beforeEach(() => { - mockRunnersCopy = [ - { - ...mockRunners[0], - }, - ]; - }); - - it('Formats job counts', () => { - mockRunnersCopy[0].jobCount = 1; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1'); - }); - - it('Formats large job counts', () => { - mockRunnersCopy[0].jobCount = 1000; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000'); - }); - - it('Formats large job counts with a plus symbol', () => { - mockRunnersCopy[0].jobCount = 1001; - - createComponent({ props: { runners: mockRunnersCopy } }, mountExtended); - - expect(findCell({ fieldKey: 'jobCount' }).text()).toBe('1,000+'); - }); - }); - it('Shows runner identifier', () => { const { id, shortSha } = mockRunners[0]; const numericId = getIdFromGraphQLId(id); @@ -226,62 +192,4 @@ describe('RunnerList', () => { expect(findSkeletonLoader().exists()).toBe(false); }); }); - - describe.each` - glFeatures - ${{ runnerListStackedLayoutAdmin: true }} - ${{ runnerListStackedLayout: true }} - `('When glFeatures = $glFeatures', ({ glFeatures }) => { - beforeEach(() => { - createComponent( - { - stubs: { - RunnerStatusPopover: { - template: '<div/>', - }, - }, - provide: { - glFeatures, - }, - }, - mountExtended, - ); - }); - - it('Displays stacked list headers', () => { - const headerLabels = findHeaders().wrappers.map((w) => w.text()); - - expect(headerLabels).toEqual([ - 'Status', - 'Runner', - '', // actions has no label - ]); - }); - - it('Displays stacked details of a runner', () => { - const { id, description, version, shortSha } = mockRunners[0]; - const numericId = getIdFromGraphQLId(id); - - // Badges - expect(findCell({ fieldKey: 'status' }).text()).toMatchInterpolatedText( - I18N_STATUS_NEVER_CONTACTED, - ); - - // Runner summary - const summary = findCell({ fieldKey: 'summary' }).text(); - - expect(summary).toContain(`#${numericId} (${shortSha})`); - expect(summary).toContain(I18N_PROJECT_TYPE); - - expect(summary).toContain(version); - expect(summary).toContain(description); - - expect(summary).toContain('Last contact'); - expect(summary).toContain('0'); // job count - expect(summary).toContain('Created'); - - // Actions - expect(findCell({ fieldKey: 'actions' }).exists()).toBe(true); - }); - }); }); diff --git a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js index b892cfc7d3d..1a8aced9292 100644 --- a/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js +++ b/spec/frontend/runner/components/runner_stacked_layout_banner_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import { nextTick } from 'vue'; import { GlBanner } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerStackedLayoutBanner from '~/runner/components/runner_stacked_layout_banner.vue'; @@ -16,42 +16,24 @@ describe('RunnerStackedLayoutBanner', () => { }); }; - it('Does not display a banner', () => { + it('Displays a banner', () => { createComponent(); - expect(findBanner().exists()).toBe(false); - }); - - describe.each` - glFeatures - ${{ runnerListStackedLayoutAdmin: true }} - ${{ runnerListStackedLayout: true }} - `('When glFeatures = $glFeatures', ({ glFeatures }) => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures, - }, - }); - }); - - it('Displays a banner', () => { - expect(findBanner().props()).toMatchObject({ - svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), - title: expect.any(String), - buttonText: expect.any(String), - buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), - }); - expect(findLocalStorageSync().exists()).toBe(true); + expect(findBanner().props()).toMatchObject({ + svgPath: expect.stringContaining('data:image/svg+xml;utf8,'), + title: expect.any(String), + buttonText: expect.any(String), + buttonLink: expect.stringContaining('https://gitlab.com/gitlab-org/gitlab/-/issues/'), }); + expect(findLocalStorageSync().exists()).toBe(true); + }); - it('Does not display a banner when dismissed', async () => { - findLocalStorageSync().vm.$emit('input', true); + it('Does not display a banner when dismissed', async () => { + findLocalStorageSync().vm.$emit('input', true); - await Vue.nextTick(); + await nextTick(); - expect(findBanner().exists()).toBe(false); - expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal - }); + expect(findBanner().exists()).toBe(false); + expect(findLocalStorageSync().exists()).toBe(true); // continues syncing after removal }); }); diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js index d843da4da5b..e5594b6d37e 100644 --- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js +++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js @@ -164,8 +164,7 @@ describe('IssuableEditForm', () => { const titleInputEl = wrapper.findComponent(GlFormInput); titleInputEl.vm.$emit('keydown', eventObj, 'title'); - - expect(wrapper.emitted('keydown-title')).toBeTruthy(); + expect(wrapper.emitted('keydown-title')).toHaveLength(1); expect(wrapper.emitted('keydown-title')[0]).toMatchObject([ eventObj, { @@ -179,8 +178,7 @@ describe('IssuableEditForm', () => { const descriptionInputEl = wrapper.find('[data-testid="description"] textarea'); descriptionInputEl.trigger('keydown', eventObj, 'description'); - - expect(wrapper.emitted('keydown-description')).toBeTruthy(); + expect(wrapper.emitted('keydown-description')).toHaveLength(1); expect(wrapper.emitted('keydown-description')[0]).toMatchObject([ eventObj, { diff --git a/spec/initializers/microsoft_graph_mailer_spec.rb b/spec/initializers/microsoft_graph_mailer_spec.rb new file mode 100644 index 00000000000..fbe667e34fe --- /dev/null +++ b/spec/initializers/microsoft_graph_mailer_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'microsoft_graph_mailer initializer for GitLab' do + let(:microsoft_graph_setting) do + { + user_id: SecureRandom.hex, + tenant: SecureRandom.hex, + client_id: SecureRandom.hex, + client_secret: SecureRandom.hex, + azure_ad_endpoint: 'https://test-azure_ad_endpoint', + graph_endpoint: 'https://test-graph_endpoint' + } + end + + def load_microsoft_graph_mailer_initializer + load Rails.root.join('config/initializers/microsoft_graph_mailer.rb') + end + + context 'when microsoft_graph_mailer is enabled' do + before do + stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: true)) + end + + it 'configures ActionMailer' do + previous_delivery_method = ActionMailer::Base.delivery_method + previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings + + load_microsoft_graph_mailer_initializer + + expect(ActionMailer::Base.delivery_method).to eq(:microsoft_graph) + expect(ActionMailer::Base.microsoft_graph_settings).to eq(microsoft_graph_setting) + ensure + ActionMailer::Base.delivery_method = previous_delivery_method + ActionMailer::Base.microsoft_graph_settings = previous_microsoft_graph_settings + end + end + + context 'when microsoft_graph_mailer is disabled' do + before do + stub_microsoft_graph_mailer_setting(microsoft_graph_setting.merge(enabled: false)) + end + + it 'does not configure ActionMailer' do + previous_delivery_method = ActionMailer::Base.delivery_method + previous_microsoft_graph_settings = ActionMailer::Base.microsoft_graph_settings + + load_microsoft_graph_mailer_initializer + + expect(previous_microsoft_graph_settings).not_to eq(:microsoft_graph) + expect(ActionMailer::Base.delivery_method).to eq(previous_delivery_method) + expect(ActionMailer::Base.microsoft_graph_settings).to eq(previous_microsoft_graph_settings) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb new file mode 100644 index 00000000000..29833074109 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' +# todo: this will need to specify schema version once we introduce the not null constraint on issues#namespace_id +# https://gitlab.com/gitlab-org/gitlab/-/issues/367835 +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectNamespaceOnIssues do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + + let(:namespace1) { namespaces.create!(name: 'batchtest1', type: 'Group', path: 'space1') } + let(:namespace2) { namespaces.create!(name: 'batchtest2', type: 'Group', parent_id: namespace1.id, path: 'space2') } + + let(:proj_namespace1) { namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id) } + let(:proj_namespace2) { namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace2.id) } + + # rubocop:disable Layout/LineLength + let(:proj1) { projects.create!(name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id) } + let(:proj2) { projects.create!(name: 'proj2', path: 'proj2', namespace_id: namespace2.id, project_namespace_id: proj_namespace2.id) } + + let!(:proj1_issue_with_namespace) { issues.create!(title: 'issue1', project_id: proj1.id, namespace_id: proj_namespace1.id) } + let!(:proj1_issue_without_namespace1) { issues.create!(title: 'issue2', project_id: proj1.id) } + let!(:proj1_issue_without_namespace2) { issues.create!(title: 'issue3', project_id: proj1.id) } + let!(:proj2_issue_with_namespace) { issues.create!(title: 'issue4', project_id: proj2.id, namespace_id: proj_namespace2.id) } + let!(:proj2_issue_without_namespace1) { issues.create!(title: 'issue5', project_id: proj2.id) } + let!(:proj2_issue_without_namespace2) { issues.create!(title: 'issue6', project_id: proj2.id) } + # rubocop:enable Layout/LineLength + + let(:migration) do + described_class.new( + start_id: proj1_issue_with_namespace.id, + end_id: proj2_issue_without_namespace2.id, + batch_table: :issues, + batch_column: :id, + sub_batch_size: 2, + pause_ms: 2, + connection: ApplicationRecord.connection + ) + end + + subject(:perform_migration) { migration.perform } + + it 'backfills namespace_id for the selected records', :aggregate_failures do + perform_migration + + expected_namespaces = [proj_namespace1.id, proj_namespace2.id] + + expect(issues.where.not(namespace_id: nil).count).to eq(6) + expect(issues.where.not(namespace_id: nil).pluck(:namespace_id).uniq).to match_array(expected_namespaces) + end + + it 'tracks timings of queries' do + expect(migration.batch_metrics.timings).to be_empty + + expect { perform_migration }.to change { migration.batch_metrics.timings } + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e1ea5c2d825..9a87911b6e8 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -420,6 +420,26 @@ RSpec.describe Gitlab::Git::Repository do end end + describe '#delete_branch' do + let(:repository) { mutable_repository } + + it 'deletes a branch' do + expect(repository.find_branch('feature')).not_to be_nil + + repository.delete_branch('feature') + + expect(repository.find_branch('feature')).to be_nil + end + + it 'deletes a fully qualified branch' do + expect(repository.find_branch('feature')).not_to be_nil + + repository.delete_branch('refs/heads/feature') + + expect(repository.find_branch('feature')).to be_nil + end + end + describe '#delete_refs' do let(:repository) { mutable_repository } diff --git a/spec/migrations/backfill_namespace_id_on_issues_spec.rb b/spec/migrations/backfill_namespace_id_on_issues_spec.rb new file mode 100644 index 00000000000..2721d7ce8f1 --- /dev/null +++ b/spec/migrations/backfill_namespace_id_on_issues_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillNamespaceIdOnIssues, :migration do + let(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of issues' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :issues, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + max_batch_size: described_class::MAX_BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index bc4ba33067f..ec03030a4b8 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1599,7 +1599,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'track artifact report' do - let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running) } + let(:pipeline) { create(:ci_pipeline, :running, :with_test_reports, status: :running, user: create(:user)) } context 'when transitioning to completed status' do %i[drop! skip! succeed! cancel!].each do |command| diff --git a/spec/requests/api/resource_state_events_spec.rb b/spec/requests/api/resource_state_events_spec.rb index 46ca9874395..5f756bc6c63 100644 --- a/spec/requests/api/resource_state_events_spec.rb +++ b/spec/requests/api/resource_state_events_spec.rb @@ -6,87 +6,8 @@ RSpec.describe API::ResourceStateEvents do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public, namespace: user.namespace) } - before_all do - project.add_developer(user) - end - - shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do - let!(:event) { create_event } - - it "returns an array of resource state events" do - url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events" - get api(url, user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - expect(json_response.first['id']).to eq(event.id) - expect(json_response.first['state']).to eq(event.state.to_s) - end - - it "returns a 404 error when eventable id not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do - let!(:event) { create_event } - - it "returns a resource state event by id" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['id']).to eq(event.id) - expect(json_response['state']).to eq(event.state.to_s) - end - - it "returns 404 when not authorized" do - parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) - private_user = create(:user) - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{event.id}", private_user) - - expect(response).to have_gitlab_http_status(:not_found) - end - - it "returns a 404 error if resource state event not found" do - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events/#{non_existing_record_id}", user) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - describe 'pagination' do - # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 - it 'returns the second page' do - create_event - event2 = create_event - - get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_state_events?page=2&per_page=1", user) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(response.headers['X-Total']).to eq '2' - expect(json_response.count).to eq(1) - expect(json_response.first['id']).to eq(event2.id) - end - end - - def create_event(state: :opened) - create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) - end + before do + parent.add_developer(user) end context 'when eventable is an Issue' do diff --git a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb index 1abf8eb562c..6d9fc4c8e34 100644 --- a/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb +++ b/spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb @@ -6,10 +6,10 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do describe '#execute', :clean_gitlab_redis_shared_state do let_it_be(:group) { create(:group, :private) } let_it_be(:project) { create(:project, group: group) } - let_it_be(:user) { create(:user) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } let(:test_event_name) { 'i_testing_test_report_uploaded' } - let(:values_delimiter) { '_' } let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter } let(:start_time) { 1.week.ago } let(:end_time) { 1.week.from_now } @@ -17,7 +17,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do subject(:track_artifact_report) { described_class.new.execute(pipeline) } context 'when pipeline has test reports' do - let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, user: user1) } before do 2.times do @@ -28,7 +28,7 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do it 'tracks the event using HLLRedisCounter' do allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) .and_call_original expect { track_artifact_report } @@ -53,19 +53,57 @@ RSpec.describe Ci::JobArtifacts::TrackArtifactReportService do end end - context 'when multiple pipelines have test reports' do - let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user) } - let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user) } + context 'when a single user started multiple pipelines with test reports' do + let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } + let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } - it 'tracks all pipelines using HLLRedisCounter' do + it 'tracks all pipelines using HLLRedisCounter by one user_id' do allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline1.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) .and_call_original allow(Gitlab::UsageDataCounters::HLLRedisCounter) .to receive(:track_event) - .with(test_event_name, values: [pipeline2.id, user.id].join(values_delimiter)) + .with(test_event_name, values: user1.id) + .and_call_original + + expect do + described_class.new.execute(pipeline1) + described_class.new.execute(pipeline2) + end + .to change { + counter.unique_events(event_names: test_event_name, + start_date: start_time, + end_date: end_time) + } + .by 1 + end + end + + context 'when multiple users started multiple pipelines with test reports' do + let_it_be(:pipeline1) { create(:ci_pipeline, :with_test_reports, project: project, user: user1) } + let_it_be(:pipeline2) { create(:ci_pipeline, :with_test_reports, project: project, user: user2) } + + it 'tracks all pipelines using HLLRedisCounter by multiple users' do + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user1.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user1.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user2.id) + .and_call_original + + allow(Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event) + .with(test_event_name, values: user2.id) .and_call_original expect do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a40f19ecf7c..c75f651fb92 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -93,25 +93,6 @@ RSpec.configure do |config| config.full_backtrace = true end - # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/297359 - if ENV['CI'] - config.after do |example| - if example.exception.is_a?(GRPC::Unavailable) - warn "=== gRPC unavailable detected, process list:" - processes = `ps -ef | grep toml` - warn processes - warn "=== free memory" - warn `free -m` - warn "=== uptime" - warn `uptime` - warn "=== Prometheus metrics:" - warn `curl -s -o log/gitaly-metrics.log http://localhost:9236/metrics` - warn "=== Taking goroutine dump in log/goroutines.log..." - warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2` - end - end - end - # Attempt to troubleshoot https://gitlab.com/gitlab-org/gitlab/-/issues/351531 config.after do |example| if example.exception.is_a?(Gitlab::Database::QueryAnalyzers::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError) diff --git a/spec/support/helpers/html_escaped_helpers.rb b/spec/support/helpers/html_escaped_helpers.rb new file mode 100644 index 00000000000..7f6825e9598 --- /dev/null +++ b/spec/support/helpers/html_escaped_helpers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module HtmlEscapedHelpers + extend self + + # Checks if +content+ contains HTML escaped tags and returns its match. + # + # It matches escaped opening and closing tags `<<name>` and + # `</<name>`. The match is discarded if the tag is inside a quoted + # attribute value. + # Foor example, `<div title="We allow # <b>bold</b>">`. + # + # @return [MatchData, nil] Returns the match or +nil+ if no match was found. + def match_html_escaped_tags(content) + match_data = %r{<\s*(?:/\s*)?\w+}.match(content) + return unless match_data + + # Escaped HTML tags are allowed inside quoted attribute values like: + # `title="Press <back>"` + return if %r{=\s*["'][^>]*\z}.match?(match_data.pre_match) + + match_data + end +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 20f46396424..c08e35912c3 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -104,6 +104,10 @@ module StubConfiguration .to receive(:sentry_clientside_dsn) { clientside_dsn } end + def stub_microsoft_graph_mailer_setting(messages) + allow(Gitlab.config.microsoft_graph_mailer).to receive_messages(to_settings(messages)) + end + def stub_kerberos_setting(messages) allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages)) end diff --git a/spec/support/shared_contexts/views/html_safe_render_shared_context.rb b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb new file mode 100644 index 00000000000..3acca60c901 --- /dev/null +++ b/spec/support/shared_contexts/views/html_safe_render_shared_context.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_context 'when rendered view has no HTML escapes', type: :view do + # Check once per example if `rendered` contains HTML escapes. + let(:rendered) do |example| + super().tap do |rendered| + next if example.metadata[:skip_html_escaped_tags_check] + + ensure_no_html_escaped_tags!(rendered, example) + end + end + + def ensure_no_html_escaped_tags!(content, example) + match_data = HtmlEscapedHelpers.match_html_escaped_tags(content) + return unless match_data + + # Truncate + pre_match = match_data.pre_match.last(50) + match = match_data[0] + post_match = match_data.post_match.first(50) + + string = "#{pre_match}«#{match}»#{post_match}" + + raise <<~MESSAGE + The following string contains HTML escaped tags: + + #{string} + + Please consider using `.html_safe`. + + This check can be disabled via: + + it #{example.description.inspect}, :skip_html_escaped_tags_check do + ... + end + + MESSAGE + end +end diff --git a/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb new file mode 100644 index 00000000000..c1850a0d0c9 --- /dev/null +++ b/spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'resource_state_events API' do |parent_type, eventable_type, id_name| + let(:base_path) { "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}" } + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events" do + let!(:event) { create_event } + + it "returns an array of resource state events" do + url = "#{base_path}/resource_state_events" + get api(url, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + expect(json_response.first['state']).to eq(event.state.to_s) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_state_events", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("#{base_path}/resource_state_events", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_state_events/:event_id" do + let!(:event) { create_event } + + it "returns a resource state event by id" do + get api("#{base_path}/resource_state_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to eq(event.id) + expect(json_response['state']).to eq(event.state.to_s) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + private_user = create(:user) + + get api("#{base_path}/resource_state_events/#{event.id}", private_user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if resource state event not found" do + get api("#{base_path}/resource_state_events/#{non_existing_record_id}", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + describe 'pagination' do + # https://gitlab.com/gitlab-org/gitlab/-/issues/220192 + it 'returns the second page' do + create_event + event2 = create_event + + get api("#{base_path}/resource_state_events?page=2&per_page=1", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(response.headers['X-Total']).to eq '2' + expect(json_response.count).to eq(1) + expect(json_response.first['id']).to eq(event2.id) + end + end + + def create_event(state: :opened) + create(:resource_state_event, eventable.class.name.underscore => eventable, state: state) + end +end diff --git a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb index 716bee39fca..a7e51408032 100644 --- a/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb +++ b/spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples 'filters by paginated notes' do |event_type| - let(:event) { create(event_type) } # rubocop:disable Rails/SaveBang + let(:event) { create(event_type, issue: create(:issue)) } before do create(event_type, issue: event.issue) diff --git a/spec/support_specs/helpers/html_escaped_helpers_spec.rb b/spec/support_specs/helpers/html_escaped_helpers_spec.rb new file mode 100644 index 00000000000..337f7ecc659 --- /dev/null +++ b/spec/support_specs/helpers/html_escaped_helpers_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +require_relative '../../support/helpers/html_escaped_helpers' + +RSpec.describe HtmlEscapedHelpers do + using RSpec::Parameterized::TableSyntax + + describe '#match_html_escaped_tags' do + let(:actual_match) { actual_match_data && actual_match_data[0] } + + subject(:actual_match_data) { described_class.match_html_escaped_tags(content) } + + where(:content, :expected_match) do + nil | nil + '' | nil + '<a href' | nil + '<span href' | nil + '</a>' | nil + '<a href' | '<a' + '<span href' | '<span' + '< span' | '< span' + 'some text <a href' | '<a' + 'some text "<a href' | '<a' + '</a&glt;' | '</a' + '</span>' | '</span' + '< / span>' | '< / span' + 'title="<a href' | nil + 'title= "<a href' | nil + "title= '<a href" | nil + "title= '</a" | nil + "title= '</span" | nil + 'title="foo"><a' | '<a' + "title='foo'>\n<a" | '<a' + end + + with_them do + specify { expect(actual_match).to eq(expected_match) } + end + end +end diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb index 7c171ee65b9..7f537022445 100644 --- a/spec/views/projects/imports/new.html.haml_spec.rb +++ b/spec/views/projects/imports/new.html.haml_spec.rb @@ -14,7 +14,7 @@ RSpec.describe "projects/imports/new.html.haml" do project.add_maintainer(user) end - it "escapes HTML in import errors" do + it "escapes HTML in import errors", :skip_html_escaped_tags_check do assign(:project, project) render diff --git a/vendor/gems/microsoft_graph_mailer/.gitlab-ci.yml b/vendor/gems/microsoft_graph_mailer/.gitlab-ci.yml new file mode 100644 index 00000000000..1b10debb1b9 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/.gitlab-ci.yml @@ -0,0 +1,32 @@ +workflow: + rules: + - if: $CI_MERGE_REQUEST_ID + +.rspec: + cache: + key: microsoft_graph_mailer-ruby + paths: + - vendor/gems/microsoft_graph_mailer/vendor/ruby + before_script: + - cd vendor/gems/microsoft_graph_mailer + - ruby -v # Print out ruby version for debugging + - gem install bundler --no-document # Bundler is not installed with the image + - bundle config set --local path 'vendor' # Install dependencies into ./vendor/ruby + - bundle config set with 'development' + - bundle config set --local frozen 'true' # Disallow Gemfile.lock changes on CI + - bundle config # Show bundler configuration + - bundle install -j $(nproc) + script: + - bundle exec rspec + +rspec-2.7: + image: "ruby:2.7" + extends: .rspec + +rspec-3.0: + image: "ruby:3.0" + extends: .rspec + +rspec-3.1: + image: "ruby:3.1" + extends: .rspec diff --git a/vendor/gems/microsoft_graph_mailer/Gemfile b/vendor/gems/microsoft_graph_mailer/Gemfile new file mode 100644 index 00000000000..be173b205f7 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gemspec diff --git a/vendor/gems/microsoft_graph_mailer/Gemfile.lock b/vendor/gems/microsoft_graph_mailer/Gemfile.lock new file mode 100644 index 00000000000..d6bb01eba73 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/Gemfile.lock @@ -0,0 +1,217 @@ +PATH + remote: . + specs: + microsoft_graph_mailer (0.1.0) + mail (~> 2.7) + oauth2 (>= 1.4.4, < 3) + +GEM + remote: https://rubygems.org/ + specs: + actioncable (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailbox (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + mail (>= 2.7.1) + net-imap + net-pop + net-smtp + actionmailer (7.0.4) + actionpack (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activesupport (= 7.0.4) + mail (~> 2.5, >= 2.5.4) + net-imap + net-pop + net-smtp + rails-dom-testing (~> 2.0) + actionpack (7.0.4) + actionview (= 7.0.4) + activesupport (= 7.0.4) + rack (~> 2.0, >= 2.2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (7.0.4) + actionpack (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.6.0) + nokogiri (>= 1.8.5) + actionview (7.0.4) + activesupport (= 7.0.4) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (7.0.4) + activesupport (= 7.0.4) + globalid (>= 0.3.6) + activemodel (7.0.4) + activesupport (= 7.0.4) + activerecord (7.0.4) + activemodel (= 7.0.4) + activesupport (= 7.0.4) + activestorage (7.0.4) + actionpack (= 7.0.4) + activejob (= 7.0.4) + activerecord (= 7.0.4) + activesupport (= 7.0.4) + marcel (~> 1.0) + mini_mime (>= 1.1.0) + activesupport (7.0.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) + builder (3.2.4) + concurrent-ruby (1.1.10) + crack (0.4.5) + rexml + crass (1.0.6) + debug (1.6.2) + irb (>= 1.3.6) + reline (>= 0.3.1) + diff-lcs (1.5.0) + digest (3.1.0) + erubi (1.11.0) + faraday (2.5.2) + faraday-net_http (>= 2.0, < 3.1) + ruby2_keywords (>= 0.0.4) + faraday-net_http (3.0.0) + globalid (1.0.0) + activesupport (>= 5.0) + hashdiff (1.0.1) + hashie (5.0.0) + i18n (1.12.0) + concurrent-ruby (~> 1.0) + io-console (0.5.11) + irb (1.4.1) + reline (>= 0.3.0) + jwt (2.5.0) + loofah (2.19.0) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.7.1) + mini_mime (>= 0.1.1) + marcel (1.0.2) + method_source (1.0.0) + mini_mime (1.1.2) + mini_portile2 (2.8.0) + minitest (5.16.3) + multi_xml (0.6.0) + net-imap (0.2.3) + digest + net-protocol + strscan + net-pop (0.1.1) + digest + net-protocol + timeout + net-protocol (0.1.3) + timeout + net-smtp (0.3.1) + digest + net-protocol + timeout + nio4r (2.5.8) + nokogiri (1.13.8) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) + oauth2 (2.0.8) + faraday (>= 0.17.3, < 3.0) + jwt (>= 1.0, < 3.0) + multi_xml (~> 0.5) + rack (>= 1.2, < 3) + snaky_hash (~> 2.0) + version_gem (~> 1.1) + public_suffix (5.0.0) + racc (1.6.0) + rack (2.2.4) + rack-test (2.0.2) + rack (>= 1.3) + rails (7.0.4) + actioncable (= 7.0.4) + actionmailbox (= 7.0.4) + actionmailer (= 7.0.4) + actionpack (= 7.0.4) + actiontext (= 7.0.4) + actionview (= 7.0.4) + activejob (= 7.0.4) + activemodel (= 7.0.4) + activerecord (= 7.0.4) + activestorage (= 7.0.4) + activesupport (= 7.0.4) + bundler (>= 1.15.0) + railties (= 7.0.4) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.4.3) + loofah (~> 2.3) + railties (7.0.4) + actionpack (= 7.0.4) + activesupport (= 7.0.4) + method_source + rake (>= 12.2) + thor (~> 1.0) + zeitwerk (~> 2.5) + rake (13.0.6) + reline (0.3.1) + io-console (~> 0.5) + rexml (3.2.5) + rspec (3.11.0) + rspec-core (~> 3.11.0) + rspec-expectations (~> 3.11.0) + rspec-mocks (~> 3.11.0) + rspec-core (3.11.0) + rspec-support (~> 3.11.0) + rspec-expectations (3.11.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-mocks (3.11.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.11.0) + rspec-support (3.11.1) + ruby2_keywords (0.0.5) + snaky_hash (2.0.0) + hashie + version_gem (~> 1.1) + strscan (3.0.4) + thor (1.2.1) + timeout (0.3.0) + tzinfo (2.0.5) + concurrent-ruby (~> 1.0) + version_gem (1.1.0) + webmock (3.18.1) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) + websocket-driver (0.7.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.5) + zeitwerk (2.6.0) + +PLATFORMS + ruby + +DEPENDENCIES + debug (>= 1.0.0) + microsoft_graph_mailer! + rails + rspec (~> 3.11.0) + webmock (~> 3.18.1) + +BUNDLED WITH + 2.3.22 diff --git a/vendor/gems/microsoft_graph_mailer/LICENSE.txt b/vendor/gems/microsoft_graph_mailer/LICENSE.txt new file mode 100644 index 00000000000..7ed79aa0423 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 GitLab B.V. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/gems/microsoft_graph_mailer/README.md b/vendor/gems/microsoft_graph_mailer/README.md new file mode 100644 index 00000000000..dd9dfecfc56 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/README.md @@ -0,0 +1,104 @@ +# microsoft_graph_mailer + +This gem allows delivery of emails using [Microsoft Graph API](https://docs.microsoft.com/en-us/graph/api/user-sendmail) with [OAuth 2.0 client credentials flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow). + +## The reason for this gem + +See [https://gitlab.com/groups/gitlab-org/-/epics/8259](https://gitlab.com/groups/gitlab-org/-/epics/8259). + +## Installation + +Add this line to your application's Gemfile: + +```ruby +gem 'microsoft_graph_mailer' +``` + +And then execute: + +```shell +bundle +``` + +Or install it yourself as: + +```shell +gem install microsoft_graph_mailer +``` + +## Settings + +To use the Microsoft Graph API to send mails, you will +need to create an application in the Azure Active Directory. See the +[Microsoft instructions](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) for more details: + +1. Sign in to the [Azure portal](https://portal.azure.com). +1. Search for and select `Azure Active Directory`. +1. Under `Manage`, select `App registrations` > `New registration`. +1. Enter a `Name` for your application, such as `MicrosoftGraphMailer`. Users of your app might see this name, and you can change it later. +1. If `Supported account types` is listed, select the appropriate option. +1. Leave `Redirect URI` blank. This is not needed. +1. Select `Register`. +1. Under `Manage`, select `Certificates & secrets`. +1. Under `Client secrets`, select `New client secret`, and enter a name. +1. Under `Expires`, select `Never`, unless you plan on updating the credentials every time it expires. +1. Select `Add`. Record the secret value in a safe location for use in a later step. +1. Under `Manage`, select `API Permissions` > `Add a permission`. Select `Microsoft Graph`. +1. Select `Application permissions`. +1. Under the `Mail` node, select `Mail.Send`. Then select Add permissions. +1. If `User.Read` is listed in the permission list, you can delete this. +1. Click `Grant admin consent` for these permissions. + +- `user_id` - The unique identifier for the user. To use Microsoft Graph on behalf of the user. +- `tenant` - The directory tenant the application plans to operate against, in GUID or domain-name format. +- `client_id` - The application ID that's assigned to your app. You can find this information in the portal where you registered your app. +- `client_secret` - The client secret that you generated for your app in the app registration portal. + +## Usage + +```ruby +require "microsoft_graph_mailer" + +microsoft_graph_mailer = MicrosoftGraphMailer::Delivery.new( + { + user_id: "YOUR-USER-ID", + tenant: "YOUR-TENANT-ID", + client_id: "YOUR-CLIENT-ID", + client_secret: "YOUR-CLIENT-SECRET-ID" + # Defaults to "https://login.microsoftonline.com". + azure_ad_endpoint: "https://login.microsoftonline.us", + # Defaults to "https://graph.microsoft.com". + graph_endpoint: "https://graph.microsoft.us" + } +) + +message = Mail.new do + from "about@gitlab.com" + to "to@example.com" + subject "GitLab Mission" + + html_part do + content_type "text/html; charset=UTF-8" + body "It is GitLab's mission to make it so that <strong>everyone can contribute</strong>." + end +end + +microsoft_graph_mailer.deliver!(message) +``` + +## Usage with ActionMailer + +```ruby +ActionMailer::Base.delivery_method = :microsoft_graph + +ActionMailer::Base.microsoft_graph_settings = { + user_id: "YOUR-USER-ID", + tenant: "YOUR-TENANT-ID", + client_id: "YOUR-CLIENT-ID", + client_secret: "YOUR-CLIENT-SECRET-ID" + # Defaults to "https://login.microsoftonline.com". + azure_ad_endpoint: "https://login.microsoftonline.us", + # Defaults to "https://graph.microsoft.com". + graph_endpoint: "https://graph.microsoft.us" +} +``` diff --git a/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer.rb b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer.rb new file mode 100644 index 00000000000..8bd252aac94 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative "microsoft_graph_mailer/delivery" +require_relative "microsoft_graph_mailer/railtie" if defined?(Rails::Railtie) +require_relative "microsoft_graph_mailer/version" + +module MicrosoftGraphMailer + class Error < StandardError + end + + class ConfigurationError < Error + end + + class DeliveryError < Error + end +end diff --git a/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/client.rb b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/client.rb new file mode 100644 index 00000000000..b779bb28c39 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/client.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "oauth2" + +module MicrosoftGraphMailer + class Client + attr_accessor :user_id, :tenant, :client_id, :client_secret, :azure_ad_endpoint, :graph_endpoint + + def initialize(user_id:, tenant:, client_id:, client_secret:, azure_ad_endpoint:, graph_endpoint:) + @user_id = user_id + @tenant = tenant + @client_id = client_id + @client_secret = client_secret + @azure_ad_endpoint = azure_ad_endpoint + @graph_endpoint = graph_endpoint + end + + def send_mail(message_in_mime_format) + # https://docs.microsoft.com/en-us/graph/api/user-sendmail + token.post( + send_mail_url, + headers: { "Content-type" => "text/plain" }, + body: Base64.encode64(message_in_mime_format) + ) + end + + private + + def token + # https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow + OAuth2::Client.new( + client_id, + client_secret, + site: azure_ad_endpoint, + token_url: "/#{tenant}/oauth2/v2.0/token" + ).client_credentials.get_token({ scope: scope }) + end + + def scope + "#{graph_endpoint}/.default" + end + + def base_url + "#{graph_endpoint}/v1.0/users/#{user_id}" + end + + def send_mail_url + "#{base_url}/sendMail" + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/delivery.rb b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/delivery.rb new file mode 100644 index 00000000000..c807ee4319e --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/delivery.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require_relative "client" + +module MicrosoftGraphMailer + class Delivery + attr_reader :microsoft_graph_settings + + def initialize(microsoft_graph_settings) + @microsoft_graph_settings = microsoft_graph_settings + + [:user_id, :tenant, :client_id, :client_secret].each do |setting| + unless microsoft_graph_settings[setting] + raise MicrosoftGraphMailer::ConfigurationError, "'#{setting}' is missing" + end + end + + @microsoft_graph_settings[:azure_ad_endpoint] ||= "https://login.microsoftonline.com" + @microsoft_graph_settings[:graph_endpoint] ||= "https://graph.microsoft.com" + end + + def deliver!(message) + # https://github.com/mikel/mail/pull/872 + if message[:bcc] + previous_message_bcc_include_in_headers = message[:bcc].include_in_headers + message[:bcc].include_in_headers = true + end + + message_in_mime_format = message.encoded + + client = MicrosoftGraphMailer::Client.new( + user_id: microsoft_graph_settings[:user_id], + tenant: microsoft_graph_settings[:tenant], + client_id: microsoft_graph_settings[:client_id], + client_secret: microsoft_graph_settings[:client_secret], + azure_ad_endpoint: microsoft_graph_settings[:azure_ad_endpoint], + graph_endpoint: microsoft_graph_settings[:graph_endpoint] + ) + + response = client.send_mail(message_in_mime_format) + + raise MicrosoftGraphMailer::DeliveryError unless response.status == 202 + + response + ensure + message[:bcc].include_in_headers = previous_message_bcc_include_in_headers if message[:bcc] + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/railtie.rb b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/railtie.rb new file mode 100644 index 00000000000..607d68cc454 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/railtie.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require_relative "delivery" + +module MicrosoftGraphMailer + class Railtie < Rails::Railtie + ActiveSupport.on_load(:action_mailer) do + add_delivery_method :microsoft_graph, MicrosoftGraphMailer::Delivery + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/version.rb b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/version.rb new file mode 100644 index 00000000000..057aebdad2b --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module MicrosoftGraphMailer + VERSION = "0.1.0" +end diff --git a/vendor/gems/microsoft_graph_mailer/microsoft_graph_mailer.gemspec b/vendor/gems/microsoft_graph_mailer/microsoft_graph_mailer.gemspec new file mode 100644 index 00000000000..af9fb5987ca --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/microsoft_graph_mailer.gemspec @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative "lib/microsoft_graph_mailer/version" + +Gem::Specification.new do |spec| + spec.name = "microsoft_graph_mailer" + spec.version = MicrosoftGraphMailer::VERSION + spec.authors = ["Bogdan Denkovych"] + spec.email = ["bdenkovych@gitlab.com"] + + spec.summary = "Allows delivery of emails using Microsoft Graph API with OAuth 2.0 client credentials flow" + spec.homepage = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/microsoft_graph_mailer" + spec.license = "MIT" + spec.required_ruby_version = ">= 2.7.0" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "https://gitlab.com/gitlab-org/gitlab/-/tree/master/vendor/gems/microsoft_graph_mailer" + + spec.files = Dir["lib/**/*.rb"] + ["LICENSE.txt", "README.md"] + spec.require_paths = ["lib"] + + spec.add_runtime_dependency "mail", "~> 2.7" + spec.add_runtime_dependency "oauth2", [">= 1.4.4", "< 3"] + + spec.add_development_dependency "debug", ">= 1.0.0" + spec.add_development_dependency "rails" + spec.add_development_dependency "rspec", "~> 3.11.0" + spec.add_development_dependency "webmock", "~> 3.18.1" +end diff --git a/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab.txt b/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab.txt new file mode 100644 index 00000000000..1df29200042 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab.txt @@ -0,0 +1 @@ +GitLab diff --git a/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png b/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png Binary files differnew file mode 100644 index 00000000000..12525056939 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png diff --git a/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb new file mode 100644 index 00000000000..23096e75b76 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "securerandom" + +RSpec.describe MicrosoftGraphMailer::Delivery do + let(:microsoft_graph_settings) do + { + user_id: SecureRandom.hex, + tenant: SecureRandom.hex, + client_id: SecureRandom.hex, + client_secret: SecureRandom.hex, + azure_ad_endpoint: "https://test-azure_ad_endpoint", + graph_endpoint: "https://test-graph_endpoint" + } + end + + subject { described_class.new(microsoft_graph_settings) } + + describe ".new" do + it "sets #microsoft_graph_settings" do + expect(subject.microsoft_graph_settings).to eq(microsoft_graph_settings) + end + + [:user_id, :tenant, :client_id, :client_secret].each do |setting| + it "raises MicrosoftGraphMailer::ConfigurationError when '#{setting}' is missing" do + microsoft_graph_settings[setting] = nil + + expect { subject } + .to raise_error(MicrosoftGraphMailer::ConfigurationError, "'#{setting}' is missing") + end + end + + it "sets azure_ad_endpoint setting to 'https://login.microsoftonline.com' when it is missing" do + microsoft_graph_settings[:azure_ad_endpoint] = nil + + expect(subject.microsoft_graph_settings[:azure_ad_endpoint]).to eq("https://login.microsoftonline.com") + end + + it "sets graph_endpoint setting to 'https://graph.microsoft.com' when it is missing" do + microsoft_graph_settings[:graph_endpoint] = nil + + expect(subject.microsoft_graph_settings[:graph_endpoint]).to eq("https://graph.microsoft.com") + end + end + + describe "#deliver!" do + let(:access_token) { SecureRandom.hex } + + let(:message) do + Mail.new do + from "about@gitlab.com" + + to "to@example.com" + + cc "cc@example.com" + + subject "GitLab Mission" + + text_part do + body "It is GitLab's mission to make it so that everyone can contribute." + end + + html_part do + content_type "text/html; charset=UTF-8" + body "It is GitLab's mission to make it so that <strong>everyone can contribute</strong>." + end + + add_file fixture_path("attachments", "gitlab.txt") + + add_file fixture_path("attachments", "gitlab_logo.png") + end + end + + context "when token request is successful" do + before do + stub_token_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, response_status: 200) + end + + context "when send mail request returns response status 202" do + it "sends mail and returns an instance of OAuth2::Response" do + stub_send_mail_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + expect(subject.deliver!(message)).to be_an_instance_of(OAuth2::Response) + end + + it "sends mail including bcc field" do + message.bcc = "bcc@example.com" + + stub_send_mail_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + subject.deliver!(message) + end + + it "does not change message[:bcc].include_in_headers" do + message.bcc = "bcc@example.com" + expected_message_bcc_include_in_headers = "42" + message[:bcc].include_in_headers = expected_message_bcc_include_in_headers + + stub_send_mail_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + subject.deliver!(message) + + expect(message[:bcc].include_in_headers).to eq(expected_message_bcc_include_in_headers) + end + end + + context "when send mail request returns response status other than 202" do + it "raises MicrosoftGraphMailer::DeliveryError" do + stub_send_mail_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, message: message, response_status: 200) + + expect { subject.deliver!(message) }.to raise_error(MicrosoftGraphMailer::DeliveryError) + end + end + end + + context "when token request is not successful" do + before do + stub_token_request(microsoft_graph_settings: subject.microsoft_graph_settings, access_token: access_token, response_status: 503) + end + + it "raises OAuth2::Error" do + expect { subject.deliver!(message) }.to raise_error(OAuth2::Error) + end + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb new file mode 100644 index 00000000000..d1a60522cdf --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require "spec_helper" + +require "securerandom" + +class TestMailer < ActionMailer::Base + def gitlab_mission(to:, cc: []) + mail(from: "about@gitlab.com", to: to, cc: cc, subject: "GitLab Mission") do |format| + format.text { render plain: "It is GitLab's mission to make it so that everyone can contribute." } + format.html { render html: "It is GitLab's mission to make it so that <strong>everyone can contribute</strong>.".html_safe } + end + + mail.attachments["gitlab.txt"] = File.read(fixture_path("attachments", "gitlab.txt")) + + mail.attachments["gitlab_logo.png"] = File.read(fixture_path("attachments", "gitlab_logo.png")) + end +end + +RSpec.describe MicrosoftGraphMailer::Railtie do + let(:microsoft_graph_settings) do + { + user_id: SecureRandom.hex, + tenant: SecureRandom.hex, + client_id: SecureRandom.hex, + client_secret: SecureRandom.hex, + azure_ad_endpoint: "https://test-azure_ad_endpoint", + graph_endpoint: "https://test-graph_endpoint" + } + end + + let(:message) { TestMailer.gitlab_mission(to: "to@example.com", cc: "cc@example.com") } + + before do + ActionMailer::Base.delivery_method = :microsoft_graph + ActionMailer::Base.microsoft_graph_settings = microsoft_graph_settings + end + + it "its superclass is Rails::Railtie" do + expect(MicrosoftGraphMailer::Railtie.superclass).to eq(Rails::Railtie) + end + + describe "settings" do + describe "ActionMailer::Base.delivery_methods[:microsoft_graph]" do + it "returns MicrosoftGraphMailer::Delivery" do + expect(ActionMailer::Base.delivery_methods[:microsoft_graph]).to eq(MicrosoftGraphMailer::Delivery) + end + end + + describe "ActionMailer::Base.microsoft_graph_settings" do + it "returns microsoft_graph_settings" do + expect(ActionMailer::Base.microsoft_graph_settings).to eq(microsoft_graph_settings) + end + end + + it "sets #microsoft_graph_settings" do + expect(message.delivery_method.microsoft_graph_settings).to eq(microsoft_graph_settings) + end + + [:user_id, :tenant, :client_id, :client_secret].each do |setting| + it "raises MicrosoftGraphMailer::ConfigurationError when '#{setting}' is missing" do + microsoft_graph_settings[setting] = nil + ActionMailer::Base.microsoft_graph_settings = microsoft_graph_settings + + expect { message.delivery_method } + .to raise_error(MicrosoftGraphMailer::ConfigurationError, "'#{setting}' is missing") + end + end + + it "sets azure_ad_endpoint setting to 'https://login.microsoftonline.com' when it is missing" do + microsoft_graph_settings[:azure_ad_endpoint] = nil + ActionMailer::Base.microsoft_graph_settings = microsoft_graph_settings + + expect(message.delivery_method.microsoft_graph_settings[:azure_ad_endpoint]).to eq("https://login.microsoftonline.com") + end + + it "sets graph_endpoint setting to 'https://graph.microsoft.com' when it is missing" do + microsoft_graph_settings[:graph_endpoint] = nil + ActionMailer::Base.microsoft_graph_settings = microsoft_graph_settings + + expect(message.delivery_method.microsoft_graph_settings[:graph_endpoint]).to eq("https://graph.microsoft.com") + end + end + + describe "ActionMailer::MessageDelivery#deliver_now" do + let(:access_token) { SecureRandom.hex } + + context "when token request is successful" do + before do + stub_token_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, response_status: 200) + end + + context "when send mail request returns response status 202" do + it "sends and returns mail" do + stub_send_mail_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + expect(message.deliver_now).to eq(message) + end + + it "sends mail including bcc field" do + message.bcc = "bcc@example.com" + + stub_send_mail_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + message.deliver_now + end + + it "does not change message[:bcc].include_in_headers" do + message.bcc = "bcc@example.com" + expected_message_bcc_include_in_headers = "42" + message[:bcc].include_in_headers = expected_message_bcc_include_in_headers + + stub_send_mail_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, message: message, response_status: 202) + + message.deliver_now + + expect(message[:bcc].include_in_headers).to eq(expected_message_bcc_include_in_headers) + end + end + + context "when send mail request returns response status other than 202" do + it "raises MicrosoftGraphMailer::DeliveryError" do + stub_send_mail_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, message: message, response_status: 200) + + expect { message.deliver_now }.to raise_error(MicrosoftGraphMailer::DeliveryError) + end + end + end + + context "when token request is not successful" do + before do + stub_token_request(microsoft_graph_settings: microsoft_graph_settings, access_token: access_token, response_status: 503) + end + + it "raises OAuth2::Error" do + expect { message.deliver_now }.to raise_error(OAuth2::Error) + end + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer_spec.rb b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer_spec.rb new file mode 100644 index 00000000000..3306e91db38 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe MicrosoftGraphMailer do + describe "::VERSION" do + it "returns a version number" do + expect(MicrosoftGraphMailer::VERSION).to eq("0.1.0") + end + end + + describe "::Error" do + it "its superclass is StandardError" do + expect(MicrosoftGraphMailer::Error.superclass).to eq(StandardError) + end + end + + describe "::ConfigurationError" do + it "its superclass is MicrosoftGraphMailer::Error" do + expect(MicrosoftGraphMailer::ConfigurationError.superclass).to eq(MicrosoftGraphMailer::Error) + end + end + + describe "::DeliveryError" do + it "its superclass is MicrosoftGraphMailer::Error" do + expect(MicrosoftGraphMailer::DeliveryError.superclass).to eq(MicrosoftGraphMailer::Error) + end + end +end diff --git a/vendor/gems/microsoft_graph_mailer/spec/spec_helper.rb b/vendor/gems/microsoft_graph_mailer/spec/spec_helper.rb new file mode 100644 index 00000000000..ac5dd817904 --- /dev/null +++ b/vendor/gems/microsoft_graph_mailer/spec/spec_helper.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require "rails" +require "action_mailer/railtie" + +require "microsoft_graph_mailer" + +require "mail" + +require "webmock/rspec" + +RSpec.configure do |config| +end + +def fixture_path(*path) + File.join(__dir__, "fixtures", path) +end + +def stub_token_request(microsoft_graph_settings:, access_token:, response_status:) + stub_request( + :post, + "#{microsoft_graph_settings[:azure_ad_endpoint]}/#{microsoft_graph_settings[:tenant]}/oauth2/v2.0/token" + ).with( + body: { + "grant_type" => "client_credentials", + "scope" => "#{microsoft_graph_settings[:graph_endpoint]}/.default" + } + ).to_return( + body: { + "token_type" => "Bearer", + "expires_in" => "3599", + "access_token" => access_token + }.to_json, + status: response_status, + headers: { "content-type" => "application/json; charset=utf-8" } + ) +end + +def stub_send_mail_request(microsoft_graph_settings:, access_token:, message:, response_status:) + if message[:bcc] + previous_message_bcc_include_in_headers = message[:bcc].include_in_headers + message[:bcc].include_in_headers = true + end + + stub_request( + :post, + "#{microsoft_graph_settings[:graph_endpoint]}/v1.0/users/#{microsoft_graph_settings[:user_id]}/sendMail" + ).with( + body: Base64.encode64(message.encoded), + headers: { + "Authorization" => "Bearer #{access_token}", + "Content-Type" => "text/plain" + } + ).to_return( + body: "", + status: response_status + ) +ensure + message[:bcc].include_in_headers = previous_message_bcc_include_in_headers if message[:bcc] +end |