Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-15 15:13:49 +00:00
parent 3107fe7203
commit 229395d3af
91 changed files with 1906 additions and 618 deletions

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -30,6 +30,13 @@ PATH
connection_pool (~> 2.0)
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:
@ -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)

View File

@ -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>

View File

@ -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>

View File

@ -1,27 +1,14 @@
<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 }">

View File

@ -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"

View File

@ -27,4 +27,5 @@
@import './pages/service_desk';
@import './pages/settings';
@import './pages/storage_quota';
@import './pages/tree';
@import './pages/users';

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -1,5 +1,4 @@
- page_title @path, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
- add_page_specific_style 'page_bundles/tree'
= render "projects/jobs/header"

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -1,5 +1,4 @@
- page_title _("Repository Analytics")
- add_page_specific_style 'page_bundles/graph_charts'
.mb-3
%h3

View File

@ -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")

View File

@ -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 })

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -883,6 +883,18 @@ Settings.git['bin_path'] ||= '/usr/bin/git'
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
#

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
e37da383a2e69e5e3157180b33017fc64af6ee009fc3dd317ae69931d37c6350

View File

@ -81,6 +81,7 @@ exceptions:
- FREE
- FTP
- GCP
- GCS
- GDK
- GDPR
- GET

View File

@ -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"
}
```

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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')

View File

@ -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')

View File

@ -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])

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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() }));

View File

@ -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);
});
});

View File

@ -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);
});
});
});

View File

@ -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);
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);
});
describe.each`
glFeatures
${{ runnerListStackedLayoutAdmin: true }}
${{ runnerListStackedLayout: true }}
`('When glFeatures = $glFeatures', ({ glFeatures }) => {
beforeEach(() => {
createComponent({
provide: {
glFeatures,
},
});
});
it('Does not display a banner when dismissed', async () => {
findLocalStorageSync().vm.$emit('input', true);
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);
});
await nextTick();
it('Does not display a banner when dismissed', async () => {
findLocalStorageSync().vm.$emit('input', true);
await Vue.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
});
});

View File

@ -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,
{

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source "https://rubygems.org"
gemspec

View File

@ -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

View File

@ -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.

View File

@ -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"
}
```

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
module MicrosoftGraphMailer
VERSION = "0.1.0"
end

View File

@ -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

View File

@ -0,0 +1 @@
GitLab

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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