Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/qa.gitlab-ci.yml22
-rw-r--r--.gitlab/ci/review-apps/qa.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml24
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml6
-rw-r--r--.gitlab/ci/vendored-gems.gitlab-ci.yml8
-rw-r--r--.rubocop_todo/style/bare_percent_literals.yml15
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/diffs/components/commit_widget.vue2
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_summary_cell.vue75
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue49
-rw-r--r--app/assets/javascripts/runner/components/runner_stacked_layout_banner.vue15
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/page_bundles/graph_charts.scss27
-rw-r--r--app/assets/stylesheets/pages/tree.scss (renamed from app/assets/stylesheets/page_bundles/tree.scss)38
-rw-r--r--app/controllers/admin/runners_controller.rb1
-rw-r--r--app/controllers/groups/runners_controller.rb3
-rw-r--r--app/services/ci/job_artifacts/track_artifact_report_service.rb3
-rw-r--r--app/views/projects/artifacts/browse.html.haml1
-rw-r--r--app/views/projects/artifacts/file.html.haml1
-rw-r--r--app/views/projects/blame/show.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml1
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/find_file/show.html.haml1
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/show.html.haml1
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/shared/_commit_well.html.haml4
-rw-r--r--config/application.rb2
-rw-r--r--config/feature_flags/development/runner_list_stacked_layout.yml8
-rw-r--r--config/feature_flags/development/runner_list_stacked_layout_admin.yml8
-rw-r--r--config/gitlab.yml.example16
-rw-r--r--config/initializers/1_settings.rb12
-rw-r--r--config/initializers/microsoft_graph_mailer.rb14
-rw-r--r--db/post_migrate/20220706145113_backfill_namespace_id_on_issues.rb28
-rw-r--r--db/schema_migrations/202207061451131
-rw-r--r--doc/.vale/gitlab/Uppercase.yml1
-rw-r--r--doc/api/resource_state_events.md106
-rw-r--r--doc/ci/jobs/ci_job_token.md6
-rw-r--r--doc/development/feature_flags/controls.md2
-rw-r--r--doc/development/feature_flags/index.md4
-rw-r--r--doc/raketasks/backup_gitlab.md48
-rw-r--r--lib/api/helpers/resource_events_helpers.rb17
-rw-r--r--lib/api/helpers/resource_label_events_helpers.rb18
-rw-r--r--lib/api/resource_label_events.rb12
-rw-r--r--lib/api/resource_state_events.rb30
-rw-r--r--lib/gitlab/background_migration/backfill_project_namespace_on_issues.rb25
-rw-r--r--lib/gitlab/git/repository.rb4
-rw-r--r--qa/qa/support/api.rb1
-rw-r--r--qa/qa/tools/ci/helpers.rb36
-rw-r--r--qa/qa/tools/ci/test_results.rb78
-rw-r--r--qa/tasks/ci.rake5
-rw-r--r--spec/config/settings_spec.rb12
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js5
-rw-r--r--spec/frontend/runner/components/cells/runner_summary_cell_spec.js99
-rw-r--r--spec/frontend/runner/components/runner_list_spec.js124
-rw-r--r--spec/frontend/runner/components/runner_stacked_layout_banner_spec.js46
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js6
-rw-r--r--spec/initializers/microsoft_graph_mailer_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_namespace_on_issues_spec.rb57
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb20
-rw-r--r--spec/migrations/backfill_namespace_id_on_issues_spec.rb32
-rw-r--r--spec/models/ci/pipeline_spec.rb2
-rw-r--r--spec/requests/api/resource_state_events_spec.rb83
-rw-r--r--spec/services/ci/job_artifacts/track_artifact_report_service_spec.rb58
-rw-r--r--spec/spec_helper.rb19
-rw-r--r--spec/support/helpers/html_escaped_helpers.rb24
-rw-r--r--spec/support/helpers/stub_configuration.rb4
-rw-r--r--spec/support/shared_contexts/views/html_safe_render_shared_context.rb39
-rw-r--r--spec/support/shared_examples/requests/api/resource_state_events_api_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/services/resource_events/synthetic_notes_builder_shared_examples.rb2
-rw-r--r--spec/support_specs/helpers/html_escaped_helpers_spec.rb43
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
-rw-r--r--vendor/gems/microsoft_graph_mailer/.gitlab-ci.yml32
-rw-r--r--vendor/gems/microsoft_graph_mailer/Gemfile5
-rw-r--r--vendor/gems/microsoft_graph_mailer/Gemfile.lock217
-rw-r--r--vendor/gems/microsoft_graph_mailer/LICENSE.txt21
-rw-r--r--vendor/gems/microsoft_graph_mailer/README.md104
-rw-r--r--vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer.rb16
-rw-r--r--vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/client.rb51
-rw-r--r--vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/delivery.rb49
-rw-r--r--vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/railtie.rb11
-rw-r--r--vendor/gems/microsoft_graph_mailer/lib/microsoft_graph_mailer/version.rb5
-rw-r--r--vendor/gems/microsoft_graph_mailer/microsoft_graph_mailer.gemspec29
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab.txt1
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.pngbin0 -> 1528 bytes
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb128
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb140
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer_spec.rb29
-rw-r--r--vendor/gems/microsoft_graph_mailer/spec/spec_helper.rb60
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'
diff --git a/Gemfile b/Gemfile
index 8d2f7915efb..dcca4729c1e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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 `&lt;<name>` and
+ # `&lt;/<name>`. The match is discarded if the tag is inside a quoted
+ # attribute value.
+ # Foor example, `<div title="We allow # &lt;b&gt;bold&lt;/b&gt;">`.
+ #
+ # @return [MatchData, nil] Returns the match or +nil+ if no match was found.
+ def match_html_escaped_tags(content)
+ match_data = %r{&lt;\s*(?:/\s*)?\w+}.match(content)
+ return unless match_data
+
+ # Escaped HTML tags are allowed inside quoted attribute values like:
+ # `title="Press &lt;back&gt;"`
+ 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
+ '&lt;a href' | '&lt;a'
+ '&lt;span href' | '&lt;span'
+ '&lt; span' | '&lt; span'
+ 'some text &lt;a href' | '&lt;a'
+ 'some text "&lt;a href' | '&lt;a'
+ '&lt;/a&glt;' | '&lt;/a'
+ '&lt;/span&gt;' | '&lt;/span'
+ '&lt; / span&gt;' | '&lt; / span'
+ 'title="&lt;a href' | nil
+ 'title= "&lt;a href' | nil
+ "title= '&lt;a href" | nil
+ "title= '&lt;/a" | nil
+ "title= '&lt;/span" | nil
+ 'title="foo">&lt;a' | '&lt;a'
+ "title='foo'>\n&lt;a" | '&lt;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
new file mode 100644
index 00000000000..12525056939
--- /dev/null
+++ b/vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png
Binary files differ
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