Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3107fe7203
commit
229395d3af
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -527,6 +527,8 @@ gem 'erubi', '~> 1.9.0'
|
|||
gem 'mail', '= 2.7.1'
|
||||
gem 'mail-smtp_pool', '~> 0.1.0', path: 'vendor/gems/mail-smtp_pool', require: false
|
||||
|
||||
gem 'microsoft_graph_mailer', '~> 0.1.0', path: 'vendor/gems/microsoft_graph_mailer'
|
||||
|
||||
# File encryption
|
||||
gem 'lockbox', '~> 0.6.2'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 }">
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -27,4 +27,5 @@
|
|||
@import './pages/service_desk';
|
||||
@import './pages/settings';
|
||||
@import './pages/storage_quota';
|
||||
@import './pages/tree';
|
||||
@import './pages/users';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- page_title @path, _('Artifacts'), "#{@build.name} (##{@build.id})", _('Jobs')
|
||||
- add_page_specific_style 'page_bundles/tree'
|
||||
|
||||
= render "projects/jobs/header"
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- page_title _("Repository Analytics")
|
||||
- add_page_specific_style 'page_bundles/graph_charts'
|
||||
|
||||
.mb-3
|
||||
%h3
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
#
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
e37da383a2e69e5e3157180b33017fc64af6ee009fc3dd317ae69931d37c6350
|
|
@ -81,6 +81,7 @@ exceptions:
|
|||
- FREE
|
||||
- FTP
|
||||
- GCP
|
||||
- GCS
|
||||
- GDK
|
||||
- GDPR
|
||||
- GET
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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')
|
|
@ -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')
|
|
@ -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])
|
||||
|
|
|
@ -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])
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() }));
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module HtmlEscapedHelpers
|
||||
extend self
|
||||
|
||||
# Checks if +content+ contains HTML escaped tags and returns its match.
|
||||
#
|
||||
# It matches escaped opening and closing tags `<<name>` and
|
||||
# `</<name>`. The match is discarded if the tag is inside a quoted
|
||||
# attribute value.
|
||||
# Foor example, `<div title="We allow # <b>bold</b>">`.
|
||||
#
|
||||
# @return [MatchData, nil] Returns the match or +nil+ if no match was found.
|
||||
def match_html_escaped_tags(content)
|
||||
match_data = %r{<\s*(?:/\s*)?\w+}.match(content)
|
||||
return unless match_data
|
||||
|
||||
# Escaped HTML tags are allowed inside quoted attribute values like:
|
||||
# `title="Press <back>"`
|
||||
return if %r{=\s*["'][^>]*\z}.match?(match_data.pre_match)
|
||||
|
||||
match_data
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
require 'rspec-parameterized'
|
||||
|
||||
require_relative '../../support/helpers/html_escaped_helpers'
|
||||
|
||||
RSpec.describe HtmlEscapedHelpers do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
describe '#match_html_escaped_tags' do
|
||||
let(:actual_match) { actual_match_data && actual_match_data[0] }
|
||||
|
||||
subject(:actual_match_data) { described_class.match_html_escaped_tags(content) }
|
||||
|
||||
where(:content, :expected_match) do
|
||||
nil | nil
|
||||
'' | nil
|
||||
'<a href' | nil
|
||||
'<span href' | nil
|
||||
'</a>' | nil
|
||||
'<a href' | '<a'
|
||||
'<span href' | '<span'
|
||||
'< span' | '< span'
|
||||
'some text <a href' | '<a'
|
||||
'some text "<a href' | '<a'
|
||||
'</a&glt;' | '</a'
|
||||
'</span>' | '</span'
|
||||
'< / span>' | '< / span'
|
||||
'title="<a href' | nil
|
||||
'title= "<a href' | nil
|
||||
"title= '<a href" | nil
|
||||
"title= '</a" | nil
|
||||
"title= '</span" | nil
|
||||
'title="foo"><a' | '<a'
|
||||
"title='foo'>\n<a" | '<a'
|
||||
end
|
||||
|
||||
with_them do
|
||||
specify { expect(actual_match).to eq(expected_match) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gemspec
|
|
@ -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
|
|
@ -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.
|
|
@ -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"
|
||||
}
|
||||
```
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module MicrosoftGraphMailer
|
||||
VERSION = "0.1.0"
|
||||
end
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
GitLab
|
BIN
vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png
vendored
Normal file
BIN
vendor/gems/microsoft_graph_mailer/spec/fixtures/attachments/gitlab_logo.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
128
vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb
vendored
Normal file
128
vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/delivery_spec.rb
vendored
Normal 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
|
140
vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb
vendored
Normal file
140
vendor/gems/microsoft_graph_mailer/spec/lib/microsoft_graph_mailer/railtie_spec.rb
vendored
Normal 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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue