Commit c80fb62c authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 4fd19599 84d6de14
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
import { createContentEditor } from '../services/create_content_editor';
import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
......@@ -55,17 +54,12 @@ export default {
extensions,
serializerConfig,
});
this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
},
mounted() {
this.$emit('initialized', this.contentEditor);
},
beforeDestroy() {
this.contentEditor.dispose();
this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
},
methods: {
displayLoadingIndicator() {
......@@ -91,7 +85,14 @@ export default {
<template>
<content-editor-provider :content-editor="contentEditor">
<div>
<editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
<editor-state-observer
@loading="displayLoadingIndicator"
@loadingSuccess="hideLoadingIndicator"
@loadingError="hideLoadingIndicator"
@docUpdate="notifyChange"
@focus="focus"
@blur="blur"
/>
<content-editor-alert />
<div
data-testid="content-editor"
......
......@@ -8,6 +8,7 @@ export default {
return {
contentEditor,
eventHub: contentEditor.eventHub,
tiptapEditor: contentEditor.tiptapEditor,
};
},
......
<script>
import { debounce } from 'lodash';
import {
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
ALERT_EVENT,
} from '../constants';
export const tiptapToComponentMap = {
update: 'docUpdate',
......@@ -7,30 +13,48 @@ export const tiptapToComponentMap = {
transaction: 'transaction',
focus: 'focus',
blur: 'blur',
alert: 'alert',
};
export const eventHubEvents = [
ALERT_EVENT,
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
];
const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
export default {
inject: ['tiptapEditor'],
inject: ['tiptapEditor', 'eventHub'],
created() {
this.disposables = [];
Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100);
const eventHandler = debounce(
(params) => this.bubbleEvent(getComponentEventName(tiptapEvent), params),
100,
);
this.tiptapEditor?.on(tiptapEvent, eventHandler);
this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
});
eventHubEvents.forEach((event) => {
const handler = (...params) => {
this.bubbleEvent(event, ...params);
};
this.eventHub.$on(event, handler);
this.disposables.push(() => this.eventHub?.$off(event, handler));
});
},
beforeDestroy() {
this.disposables.forEach((dispose) => dispose());
},
methods: {
handleTipTapEvent(tiptapEvent, params) {
this.$emit(getComponentEventName(tiptapEvent), params);
bubbleEvent(eventHubEvent, params) {
this.$emit(eventHubEvent, params);
},
},
render() {
......
......@@ -42,9 +42,10 @@ export const TEXT_STYLE_DROPDOWN_ITEMS = [
},
];
export const LOADING_CONTENT_EVENT = 'loadingContent';
export const LOADING_CONTENT_EVENT = 'loading';
export const LOADING_SUCCESS_EVENT = 'loadingSuccess';
export const LOADING_ERROR_EVENT = 'loadingError';
export const ALERT_EVENT = 'alert';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
......@@ -56,3 +57,4 @@ export const EXTENSION_PRIORITY_LOWER = 75;
* https://tiptap.dev/guide/custom-extensions/#priority
*/
export const EXTENSION_PRIORITY_DEFAULT = 100;
export const EXTENSION_PRIORITY_HIGHEST = 200;
......@@ -9,15 +9,22 @@ export default Extension.create({
return {
uploadsPath: null,
renderMarkdown: null,
eventHub: null,
};
},
addCommands() {
return {
uploadAttachment: ({ file }) => () => {
const { uploadsPath, renderMarkdown } = this.options;
const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
return handleFileEvent({
file,
uploadsPath,
renderMarkdown,
editor: this.editor,
eventHub,
});
},
};
},
......@@ -29,23 +36,25 @@ export default Extension.create({
key: new PluginKey('attachment'),
props: {
handlePaste: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({
editor,
file: event.clipboardData.files[0],
uploadsPath,
renderMarkdown,
eventHub,
});
},
handleDrop: (_, event) => {
const { uploadsPath, renderMarkdown } = this.options;
const { uploadsPath, renderMarkdown, eventHub } = this.options;
return handleFileEvent({
editor,
file: event.dataTransfer.files[0],
uploadsPath,
renderMarkdown,
eventHub,
});
},
},
......
import eventHubFactory from '~/helpers/event_hub_factory';
import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer }) {
constructor({ tiptapEditor, serializer, eventHub }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._eventHub = eventHubFactory();
this._eventHub = eventHub;
}
get tiptapEditor() {
return this._tiptapEditor;
}
get eventHub() {
return this._eventHub;
}
get empty() {
const doc = this.tiptapEditor?.state.doc;
......@@ -23,39 +26,23 @@ export class ContentEditor {
this.tiptapEditor.destroy();
}
once(type, handler) {
this._eventHub.$once(type, handler);
}
on(type, handler) {
this._eventHub.$on(type, handler);
}
emit(type, params = {}) {
this._eventHub.$emit(type, params);
}
off(type, handler) {
this._eventHub.$off(type, handler);
}
disposeAllEvents() {
this._eventHub.dispose();
}
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _serializer: serializer } = this;
const { _tiptapEditor: editor, _serializer: serializer, _eventHub: eventHub } = this;
try {
this._eventHub.$emit(LOADING_CONTENT_EVENT);
eventHub.$emit(LOADING_CONTENT_EVENT);
const document = await serializer.deserialize({
schema: editor.schema,
content: serializedContent,
});
editor.commands.setContent(document);
this._eventHub.$emit(LOADING_SUCCESS_EVENT);
eventHub.$emit(LOADING_SUCCESS_EVENT);
} catch (e) {
this._eventHub.$emit(LOADING_ERROR_EVENT, e);
eventHub.$emit(LOADING_ERROR_EVENT, e);
throw e;
}
}
......
import { Editor } from '@tiptap/vue-2';
import { isFunction } from 'lodash';
import eventHubFactory from '~/helpers/event_hub_factory';
import { PROVIDE_SERIALIZER_OR_RENDERER_ERROR } from '../constants';
import Attachment from '../extensions/attachment';
import Audio from '../extensions/audio';
......@@ -78,8 +79,10 @@ export const createContentEditor = ({
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
const eventHub = eventHubFactory();
const builtInContentEditorExtensions = [
Attachment.configure({ uploadsPath, renderMarkdown }),
Attachment.configure({ uploadsPath, renderMarkdown, eventHub }),
Audio,
Blockquote,
Bold,
......@@ -137,5 +140,5 @@ export const createContentEditor = ({
const tiptapEditor = createTiptapEditor({ extensions: trackedExtensions, ...tiptapOptions });
const serializer = createMarkdownSerializer({ render: renderMarkdown, serializerConfig });
return new ContentEditor({ tiptapEditor, serializer });
return new ContentEditor({ tiptapEditor, serializer, eventHub });
};
......@@ -49,7 +49,7 @@ export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
return extractAttachmentLinkUrl(rendered);
};
const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
const encodedSrc = await readFileAsDataURL(file);
const { view } = editor;
......@@ -72,14 +72,14 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
editor.emit('alert', {
eventHub.$emit('alert', {
message: __('An error occurred while uploading the image. Please try again.'),
variant: 'danger',
});
}
};
const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) => {
const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
await Promise.resolve();
const { view } = editor;
......@@ -103,23 +103,23 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
editor.emit('alert', {
eventHub.$emit('alert', {
message: __('An error occurred while uploading the file. Please try again.'),
variant: 'danger',
});
}
};
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
export const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown, eventHub }) => {
if (!file) return false;
if (acceptedMimes.image.includes(file?.type)) {
uploadImage({ editor, file, uploadsPath, renderMarkdown });
uploadImage({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true;
}
uploadAttachment({ editor, file, uploadsPath, renderMarkdown });
uploadAttachment({ editor, file, uploadsPath, renderMarkdown, eventHub });
return true;
};
......@@ -288,7 +288,6 @@ export default {
'sidebar-collapsed': !isSidebarOpen,
'has-archived-block': job.archived,
}"
:erase-path="job.erase_path"
:size="jobLogSize"
:raw-path="job.raw_path"
:is-scroll-bottom-disabled="isScrollBottomDisabled"
......@@ -325,6 +324,7 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
:erase-path="job.erase_path"
:artifact-help-url="artifactHelpUrl"
data-testid="job-sidebar"
/>
......
......@@ -5,7 +5,6 @@ import { __, s__, sprintf } from '~/locale';
export default {
i18n: {
eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
scrollToBottomButtonLabel: s__('Job|Scroll to bottom'),
scrollToTopButtonLabel: s__('Job|Scroll to top'),
showRawButtonLabel: s__('Job|Show complete raw'),
......@@ -18,11 +17,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
props: {
erasePath: {
type: String,
required: false,
default: null,
},
size: {
type: Number,
required: true,
......@@ -97,20 +91,6 @@ export default {
data-testid="job-raw-link-controller"
icon="doc-text"
/>
<gl-button
v-if="erasePath"
v-gl-tooltip.body
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
:href="erasePath"
:data-confirm="__('Are you sure you want to erase this build?')"
class="gl-ml-3"
data-testid="job-log-erase-link"
data-confirm-btn-variant="danger"
data-method="post"
icon="remove"
/>
<!-- eo links -->
<!-- scroll buttons -->
......
<script>
import { GlButton, GlLink, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { JOB_SIDEBAR } from '../constants';
......@@ -10,7 +10,6 @@ export default {
},
components: {
GlButton,
GlLink,
},
directives: {
GlModal: GlModalDirective,
......@@ -37,9 +36,18 @@ export default {
:aria-label="$options.i18n.retryLabel"
category="primary"
variant="confirm"
>{{ $options.i18n.retryLabel }}</gl-button
>
<gl-link v-else :href="href" class="btn gl-button btn-confirm" data-method="post" rel="nofollow"
>{{ $options.i18n.retryLabel }}
</gl-link>
icon="retry"
data-testid="retry-job-button"
/>
<gl-button
v-else
:href="href"
:aria-label="$options.i18n.retryLabel"
category="primary"
variant="confirm"
icon="retry"
data-method="post"
data-testid="retry-job-link"
/>
</template>
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { s__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_SIDEBAR } from '../constants';
import ArtifactsBlock from './artifacts_block.vue';
......@@ -18,10 +19,17 @@ export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
export default {
name: 'JobSidebar',
i18n: {
eraseLogButtonLabel: s__('Job|Erase job log and artifacts'),
eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'),
cancelJobButtonLabel: s__('Job|Cancel'),
retryJobButtonLabel: s__('Job|Retry'),
...JOB_SIDEBAR,
},
borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
forwardDeploymentFailureModalId,
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
ArtifactsBlock,
CommitBlock,
......@@ -41,6 +49,11 @@ export default {
required: false,
default: '',
},
erasePath: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
......@@ -81,8 +94,24 @@ export default {
</h4>
</tooltip-on-truncate>
<div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
<gl-button
v-if="erasePath"
v-gl-tooltip.left
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
:href="erasePath"
:data-confirm="$options.i18n.eraseLogConfirmText"
class="gl-mr-2"
data-testid="job-log-erase-link"
data-confirm-btn-variant="danger"
data-method="post"
icon="remove"
/>
<job-sidebar-retry-button
v-if="job.retry_path"
v-gl-tooltip.left
:title="$options.i18n.retryJobButtonLabel"
:aria-label="$options.i18n.retryJobButtonLabel"
:category="retryButtonCategory"
:href="job.retry_path"
:modal-id="$options.forwardDeploymentFailureModalId"
......@@ -92,12 +121,15 @@ export default {
/>
<gl-button
v-if="job.cancel_path"
v-gl-tooltip.left
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
:href="job.cancel_path"
icon="cancel"
data-method="post"
data-testid="cancel-button"
rel="nofollow"
>{{ $options.i18n.cancel }}
</gl-button>
/>
</div>
<gl-button
......
---
name: use_received_header_for_incoming_emails
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81489
rollout_issue_url:
milestone: '14.9'
type: development
group: group::certify
default_enabled: true
......@@ -140,7 +140,7 @@ to its **Pipelines** tab.
![Pipelines index page](img/pipelines_index_v13_0.png)
Click a pipeline to open the **Pipeline Details** page and show
Select a pipeline to open the **Pipeline Details** page and show
the jobs that were run for that pipeline. From here you can cancel a running pipeline,
retry jobs on a failed pipeline, or [delete a pipeline](#delete-a-pipeline).
......@@ -246,7 +246,7 @@ For each `var` or `file_var`, a key and value are required.
[Manual jobs](../jobs/job_control.md#create-a-job-that-must-be-run-manually),
allow you to require manual interaction before moving forward in the pipeline.
You can do this straight from the pipeline graph. Just click the play button
You can do this straight from the pipeline graph. Just select the play button
to execute that particular job.
For example, your pipeline can start automatically, but require a manual action to
......@@ -259,8 +259,8 @@ In the example below, the `production` stage has a job with a manual action:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/27188) in GitLab 11.11.
Multiple manual actions in a single stage can be started at the same time using the "Play all manual" button.
After you click this button, each individual manual action is triggered and refreshed
Multiple manual actions in a single stage can be started at the same time using the "Play all manual"
After you select this action, each individual manual action is triggered and refreshed
to an updated status.
This functionality is only available:
......@@ -283,9 +283,9 @@ pipelines.
Users with the Owner role for a project can delete a pipeline
by clicking on the pipeline in the **CI/CD > Pipelines** to get to the **Pipeline Details**
page, then using the **Delete** button.
page, then selecting **Delete**.
![Pipeline Delete Button](img/pipeline-delete.png)
![Pipeline Delete](img/pipeline-delete.png)
WARNING:
Deleting a pipeline expires all pipeline caches, and deletes all related objects,
......@@ -314,7 +314,7 @@ sensitive information like deployment credentials and tokens.
**Runners** marked as **protected** can run jobs only on protected
branches, preventing untrusted code from executing on the protected runner and
preserving deployment keys and other credentials from being unintentionally
accessed. In order to ensure that jobs intended to be executed on protected
accessed. To ensure that jobs intended to be executed on protected
runners do not use regular runners, they must be tagged accordingly.
### How pipeline duration is calculated
......@@ -434,7 +434,7 @@ fix it.
Pipeline mini graphs only display jobs by stage.
Stages in pipeline mini graphs are collapsible. Hover your mouse over them and click to expand their jobs.
Stages in pipeline mini graphs are expandable. Hover your mouse over each stage to see the name and status, and select a stage to expand its jobs list.
| Mini graph | Mini graph expanded |
|:-------------------------------------------------------------|:---------------------------------------------------------------|
......
......@@ -1191,7 +1191,7 @@ has a longer discussion explaining the potential problems.
To prevent writes to the Git repository data, there are two possible approaches:
- Use [maintenance mode](../administration/maintenance_mode/index.md) **(PREMIUM SELF)** to place GitLab in a read-only state.
- Use [maintenance mode](../administration/maintenance_mode/index.md) to place GitLab in a read-only state.
- Create explicit downtime by stopping all Gitaly services before backing up the repositories:
```shell
......@@ -1354,15 +1354,13 @@ To prepare the new server:
```shell
sudo rm -f /var/opt/gitlab/redis/dump.rdb
sudo chown <your-linux-username> /var/opt/gitlab/redis
sudo mkdir /var/opt/gitlab/backups
sudo chown <your-linux-username> /var/opt/gitlab/backups
sudo chown <your-linux-username> /var/opt/gitlab/redis /var/opt/gitlab/backups
```
### Prepare and transfer content from the old server
1. Ensure you have an up-to-date system-level backup or snapshot of the old server.
1. Enable [maintenance mode](../administration/maintenance_mode/index.md) **(PREMIUM SELF)**,
1. Enable [maintenance mode](../administration/maintenance_mode/index.md),
if supported by your GitLab edition.
1. Block new CI/CD jobs from starting:
1. Edit `/etc/gitlab/gitlab.rb`, and set the following:
......@@ -1465,7 +1463,7 @@ To prepare the new server:
1. While still under the Sidekiq dashboard, select **Cron** and then **Enable All**
to re-enable periodic background jobs.
1. Test that read-only operations on the GitLab instance work as expected. For example, browse through project repository files, merge requests, and issues.
1. Disable [Maintenance Mode](../administration/maintenance_mode/index.md) **(PREMIUM SELF)**, if previously enabled.
1. Disable [Maintenance Mode](../administration/maintenance_mode/index.md), if previously enabled.
1. Test that the GitLab instance is working as expected.
1. If applicable, re-enable [incoming email](../administration/incoming_email.md) and test it is working as expected.
1. Update your DNS or load balancer to point at the new server.
......
......@@ -12,12 +12,49 @@ instance entirely offline.
## Installation
NOTE:
This guide assumes the server is Ubuntu 18.04. Instructions for other servers may vary.
This guide also assumes the server host resolves as `my-host`, which you should replace with your
server's name.
This guide assumes the server is Ubuntu 20.04 using the [Omnibus installation method](https://docs.gitlab.com/omnibus/) and will be running GitLab [Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/). Instructions for other servers may vary.
This guide also assumes the server host resolves as `my-host.internal`, which you should replace with your
server's FQDN, and that you have acess to a different server with Internet access to download the required package files.
Follow the installation instructions [as outlined in the omnibus install
guide](https://about.gitlab.com/install/#ubuntu), but make sure to specify an `http`
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a video walkthrough of this process, see [Offline GitLab Installation: Downloading & Installing](https://www.youtube.com/watch?v=TJaq4ua2Prw).
### Download the GitLab package
You should [manually download the GitLab package](../../update/package/index.md#upgrade-using-a-manually-downloaded-package) and relevant dependencies using a server of the same operating system type that has access to the Internet.
If your offline environment has no local network access, you must manually transport across the relevant package files through physical media, such as a USB drive or writable DVD.
In Ubuntu, this can be performed on a server with Internet access using the following commands:
```shell
# Download the bash script to prepare the repository
curl --silent "https://packages.gitlab.com/install/repositories/gitlab/gitlab-ee/script.deb.sh" | sudo bash
# Download the gitlab-ee package and dependencies to /var/cache/apt/archives
sudo apt-get install --download-only gitlab-ee
# Copy the contents of the apt download folder to a mounted media device
sudo cp /var/cache/apt/archives/*.deb /path/to/mount
```
### Install the GitLab package
Prerequisites:
- Before installing the GitLab package on your offline environment, ensure that you have installed all required dependencies first.
If you are using Ubuntu, you can install the dependency `.deb` packages you copied across with `dpkg`. Do not install the GitLab package yet.
```shell
# Navigate to the physical media device
sudo cd /path/to/mount
# Install the dependency packages
sudo dpkg -i <package_name>.deb
```
[Use the relevant commands for your operating system to install the package](../../update/package/index.md#upgrade-using-a-manually-downloaded-package) but make sure to specify an `http`
URL for the `EXTERNAL_URL` installation step. Once installed, we can manually
configure the SSL ourselves.
......@@ -25,8 +62,10 @@ It is strongly recommended to setup a domain for IP resolution rather than bind
to the server's IP address. This better ensures a stable target for our certs' CN
and makes long-term resolution simpler.
The following example for Ubuntu specifies the `EXTERNAL_URL` using HTTP and installs the GitLab package:
```shell
sudo EXTERNAL_URL="http://my-host.internal" apt-get install gitlab-ee
sudo EXTERNAL_URL="http://my-host.internal" dpkg -i <gitlab_package_name>.deb
```
## Enabling SSL
......@@ -38,7 +77,7 @@ Follow these steps to enable SSL for your fresh instance. Note that these steps
```ruby
# Update external_url from "http" to "https"
external_url "https://gitlab.example.com"
external_url "https://my-host.internal"
# Set Let's Encrypt to false
letsencrypt['enable'] = false
......
......@@ -8,6 +8,8 @@ module Gitlab
class Receiver
include Gitlab::Utils::StrongMemoize
RECEIVED_HEADER_REGEX = /for\s+\<(.+)\>/.freeze
def initialize(raw)
@raw = raw
end
......@@ -37,6 +39,8 @@ module Gitlab
delivered_to: delivered_to.map(&:value),
envelope_to: envelope_to.map(&:value),
x_envelope_to: x_envelope_to.map(&:value),
# reduced down to what looks like an email in the received headers
received_recipients: recipients_from_received_headers,
meta: {
client_id: "email/#{mail.from.first}",
project: handler&.project&.full_path
......@@ -82,7 +86,8 @@ module Gitlab
find_key_from_references ||
find_key_from_delivered_to_header ||
find_key_from_envelope_to_header ||
find_key_from_x_envelope_to_header
find_key_from_x_envelope_to_header ||
find_first_key_from_received_headers
end
def ensure_references_array(references)
......@@ -117,6 +122,10 @@ module Gitlab
Array(mail[:x_envelope_to])
end
def received
Array(mail[:received])
end
def find_key_from_delivered_to_header
delivered_to.find do |header|
key = email_class.key_from_address(header.value)
......@@ -138,6 +147,21 @@ module Gitlab
end
end
def find_first_key_from_received_headers
return unless ::Feature.enabled?(:use_received_header_for_incoming_emails, default_enabled: :yaml)
recipients_from_received_headers.find do |email|
key = email_class.key_from_address(email)
break key if key
end
end
def recipients_from_received_headers
strong_memoize :emails_from_received_headers do
received.map { |header| header.value[RECEIVED_HEADER_REGEX, 1] }.compact
end
end
def ignore_auto_reply!
if auto_submitted? || auto_replied?
raise AutoGeneratedEmailError
......
......@@ -4738,9 +4738,6 @@ msgstr ""
msgid "Are you sure you want to discard your changes?"
msgstr ""
msgid "Are you sure you want to erase this build?"
msgstr ""
msgid "Are you sure you want to import %d repository?"
msgid_plural "Are you sure you want to import %d repositories?"
msgstr[0] ""
......@@ -21031,9 +21028,15 @@ msgstr ""
msgid "Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. Retrying this job could result in overwriting the environment with the older source code."
msgstr ""
msgid "Job|Are you sure you want to erase this job log and artifacts?"
msgstr ""
msgid "Job|Browse"
msgstr ""
msgid "Job|Cancel"
msgstr ""
msgid "Job|Complete Raw"
msgstr ""
......@@ -21061,6 +21064,9 @@ msgstr ""
msgid "Job|Pipeline"
msgstr ""
msgid "Job|Retry"
msgstr ""
msgid "Job|Scroll to bottom"
msgstr ""
......
......@@ -313,7 +313,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'job is cancelable' do
it 'shows cancel button' do
click_link 'Cancel'
find('[data-testid="cancel-button"]').click
expect(page.current_path).to eq(job_url)
end
......@@ -1031,7 +1031,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'loads the page and shows all needed controls' do
expect(page).to have_content 'Retry'
expect(page).to have_selector('[data-testid="retry-button"')
end
end
end
......@@ -1049,7 +1049,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'shows the right status and buttons' do
page.within('aside.right-sidebar') do
expect(page).to have_content 'Cancel'
expect(page).to have_selector('[data-testid="cancel-button"')
end
end
end
......
Return-Path: <jake@example.com>
Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Received: from blabla.google.com (blabla.google.com. [1.1.1.1])
by bla.google.com with SMTPS id something.1.1.1.1.1.1.1
for <incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com>
(Google Transport Security);
Mon, 21 Feb 2022 14:41:58 -0800 (PST)
Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@example.com>; Thu, 13 Jun 2013 17:03:50 -0400
From: "jake@example.com" <jake@example.com>
To: "support@example.com" <support@example.com>
Subject: Insert hilarious subject line here
Date: Tue, 26 Nov 2019 14:22:41 +0000
Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT>
Accept-Language: de-DE, en-US
Content-Language: de-DE
X-MS-Has-Attach:
X-MS-TNEF-Correlator:
x-ms-exchange-transport-fromentityheader: Hosted
x-originating-ip: [62.96.54.178]
Content-Type: multipart/alternative;
boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_"
MIME-Version: 1.0
--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
Content-Type: text/plain; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
Content-Type: text/html; charset="iso-8859-1"
Content-Transfer-Encoding: quoted-printable
Look, a message with no Delivered-To header! Let's fallback to Received: in case it's there.
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq-gitlabhq-project_id-auth_token-issue-issue_iid@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq-gitlabhq-project_id-auth_token-issue-issue_iid@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
......
......@@ -3,20 +3,25 @@ import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditorAlert from '~/content_editor/components/content_editor_alert.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import { createTestEditor, emitEditorEvent } from '../test_utils';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/content_editor_alert', () => {
let wrapper;
let tiptapEditor;
let eventHub;
const findErrorAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = async () => {
tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
wrapper = shallowMountExtended(ContentEditorAlert, {
provide: {
tiptapEditor,
eventHub,
},
stubs: {
EditorStateObserver,
......@@ -37,7 +42,9 @@ describe('content_editor/components/content_editor_alert', () => {
async ({ message, variant }) => {
createWrapper();
await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message, variant } });
eventHub.$emit(ALERT_EVENT, { message, variant });
await nextTick();
expect(findErrorAlert().text()).toBe(message);
expect(findErrorAlert().attributes().variant).toBe(variant);
......@@ -48,11 +55,9 @@ describe('content_editor/components/content_editor_alert', () => {
const message = 'error message';
createWrapper();
await emitEditorEvent({ tiptapEditor, event: 'alert', params: { message } });
eventHub.$emit(ALERT_EVENT, { message });
await nextTick();
findErrorAlert().vm.$emit('dismiss');
await nextTick();
expect(findErrorAlert().exists()).toBe(false);
......
......@@ -121,7 +121,7 @@ describe('ContentEditor', () => {
beforeEach(async () => {
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick();
});
......@@ -143,9 +143,9 @@ describe('ContentEditor', () => {
beforeEach(async () => {
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick();
contentEditor.emit(LOADING_SUCCESS_EVENT);
contentEditor.eventHub.$emit(LOADING_SUCCESS_EVENT);
await nextTick();
});
......@@ -164,9 +164,9 @@ describe('ContentEditor', () => {
beforeEach(async () => {
createWrapper();
contentEditor.emit(LOADING_CONTENT_EVENT);
contentEditor.eventHub.$emit(LOADING_CONTENT_EVENT);
await nextTick();
contentEditor.emit(LOADING_ERROR_EVENT, error);
contentEditor.eventHub.$emit(LOADING_ERROR_EVENT, error);
await nextTick();
});
......
......@@ -3,6 +3,13 @@ import { each } from 'lodash';
import EditorStateObserver, {
tiptapToComponentMap,
} from '~/content_editor/components/editor_state_observer.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import {
LOADING_CONTENT_EVENT,
LOADING_SUCCESS_EVENT,
LOADING_ERROR_EVENT,
ALERT_EVENT,
} from '~/content_editor/constants';
import { createTestEditor } from '../test_utils';
describe('content_editor/components/editor_state_observer', () => {
......@@ -11,19 +18,29 @@ describe('content_editor/components/editor_state_observer', () => {
let onDocUpdateListener;
let onSelectionUpdateListener;
let onTransactionListener;
let onLoadingContentListener;
let onLoadingSuccessListener;
let onLoadingErrorListener;
let onAlertListener;
let eventHub;
const buildEditor = () => {
tiptapEditor = createTestEditor();
eventHub = eventHubFactory();
jest.spyOn(tiptapEditor, 'on');
};
const buildWrapper = () => {
wrapper = shallowMount(EditorStateObserver, {
provide: { tiptapEditor },
provide: { tiptapEditor, eventHub },
listeners: {
docUpdate: onDocUpdateListener,
selectionUpdate: onSelectionUpdateListener,
transaction: onTransactionListener,
[ALERT_EVENT]: onAlertListener,
[LOADING_CONTENT_EVENT]: onLoadingContentListener,
[LOADING_SUCCESS_EVENT]: onLoadingSuccessListener,
[LOADING_ERROR_EVENT]: onLoadingErrorListener,
},
});
};
......@@ -32,8 +49,11 @@ describe('content_editor/components/editor_state_observer', () => {
onDocUpdateListener = jest.fn();
onSelectionUpdateListener = jest.fn();
onTransactionListener = jest.fn();
onAlertListener = jest.fn();
onLoadingSuccessListener = jest.fn();
onLoadingContentListener = jest.fn();
onLoadingErrorListener = jest.fn();
buildEditor();
buildWrapper();
});
afterEach(() => {
......@@ -44,6 +64,8 @@ describe('content_editor/components/editor_state_observer', () => {
it('emits update, selectionUpdate, and transaction events', () => {
const content = '<p>My paragraph</p>';
buildWrapper();
tiptapEditor.commands.insertContent(content);
expect(onDocUpdateListener).toHaveBeenCalledWith(
......@@ -58,10 +80,27 @@ describe('content_editor/components/editor_state_observer', () => {
});
});
it.each`
event | listener
${ALERT_EVENT} | ${() => onAlertListener}
${LOADING_CONTENT_EVENT} | ${() => onLoadingContentListener}
${LOADING_SUCCESS_EVENT} | ${() => onLoadingSuccessListener}
${LOADING_ERROR_EVENT} | ${() => onLoadingErrorListener}
`('listens to $event event in the eventBus object', ({ event, listener }) => {
const args = {};
buildWrapper();
eventHub.$emit(event, args);
expect(listener()).toHaveBeenCalledWith(args);
});
describe('when component is destroyed', () => {
it('removes onTiptapDocUpdate and onTiptapSelectionUpdate hooks', () => {
jest.spyOn(tiptapEditor, 'off');
buildWrapper();
wrapper.destroy();
each(tiptapToComponentMap, (_, tiptapEvent) => {
......@@ -71,5 +110,25 @@ describe('content_editor/components/editor_state_observer', () => {
);
});
});
it.each`
event
${ALERT_EVENT}
${LOADING_CONTENT_EVENT}
${LOADING_SUCCESS_EVENT}
${LOADING_ERROR_EVENT}
`('removes $event event hook from eventHub', ({ event }) => {
jest.spyOn(eventHub, '$off');
jest.spyOn(eventHub, '$on');
buildWrapper();
wrapper.destroy();
expect(eventHub.$off).toHaveBeenCalledWith(
event,
eventHub.$on.mock.calls.find(([eventName]) => eventName === event)[1],
);
});
});
});
......@@ -2,6 +2,7 @@ import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import ToolbarButton from '~/content_editor/components/toolbar_button.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_button', () => {
......@@ -25,6 +26,7 @@ describe('content_editor/components/toolbar_button', () => {
},
provide: {
tiptapEditor,
eventHub: eventHubFactory(),
},
propsData: {
contentType: CONTENT_TYPE,
......
import { GlDropdown, GlButton, GlFormInputGroup } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarLinkButton from '~/content_editor/components/toolbar_link_button.vue';
import eventHubFactory from '~/helpers/event_hub_factory';
import Link from '~/content_editor/extensions/link';
import { hasSelection } from '~/content_editor/services/utils';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
......@@ -15,6 +16,7 @@ describe('content_editor/components/toolbar_link_button', () => {
wrapper = mountExtended(ToolbarLinkButton, {
provide: {
tiptapEditor: editor,
eventHub: eventHubFactory(),
},
});
};
......
......@@ -4,6 +4,7 @@ import EditorStateObserver from '~/content_editor/components/editor_state_observ
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '~/content_editor/constants';
import Heading from '~/content_editor/extensions/heading';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_text_style_dropdown', () => {
......@@ -27,6 +28,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
},
provide: {
tiptapEditor,
eventHub: eventHubFactory(),
},
propsData: {
...propsData,
......
......@@ -5,6 +5,7 @@ import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import httpStatus from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
......@@ -25,6 +26,7 @@ describe('content_editor/extensions/attachment', () => {
let link;
let renderMarkdown;
let mock;
let eventHub;
const uploadsPath = '/uploads/';
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
......@@ -50,9 +52,15 @@ describe('content_editor/extensions/attachment', () => {
beforeEach(() => {
renderMarkdown = jest.fn();
eventHub = eventHubFactory();
tiptapEditor = createTestEditor({
extensions: [Loading, Link, Image, Attachment.configure({ renderMarkdown, uploadsPath })],
extensions: [
Loading,
Link,
Image,
Attachment.configure({ renderMarkdown, uploadsPath, eventHub }),
],
});
({
......@@ -160,7 +168,7 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
tiptapEditor.on('alert', ({ message }) => {
eventHub.$on('alert', ({ message }) => {
expect(message).toBe('An error occurred while uploading the image. Please try again.');
done();
});
......@@ -236,7 +244,7 @@ describe('content_editor/extensions/attachment', () => {
it('emits an alert event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
tiptapEditor.on('alert', ({ message }) => {
eventHub.$on('alert', ({ message }) => {
expect(message).toBe('An error occurred while uploading the file. Please try again.');
done();
});
......
......@@ -4,19 +4,21 @@ import {
LOADING_ERROR_EVENT,
} from '~/content_editor/constants';
import { ContentEditor } from '~/content_editor/services/content_editor';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor } from '../test_utils';
describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let eventHub;
beforeEach(() => {
const tiptapEditor = createTestEditor();
jest.spyOn(tiptapEditor, 'destroy');
serializer = { deserialize: jest.fn() };
contentEditor = new ContentEditor({ tiptapEditor, serializer });
eventHub = eventHubFactory();
contentEditor = new ContentEditor({ tiptapEditor, serializer, eventHub });
});
describe('.dispose', () => {
......@@ -34,13 +36,13 @@ describe('content_editor/services/content_editor', () => {
serializer.deserialize.mockResolvedValueOnce('');
});
it('emits loadingContent and loadingSuccess event', () => {
it('emits loadingContent and loadingSuccess event in the eventHub', () => {
let loadingContentEmitted = false;
contentEditor.on(LOADING_CONTENT_EVENT, () => {
eventHub.$on(LOADING_CONTENT_EVENT, () => {
loadingContentEmitted = true;
});
contentEditor.on(LOADING_SUCCESS_EVENT, () => {
eventHub.$on(LOADING_SUCCESS_EVENT, () => {
expect(loadingContentEmitted).toBe(true);
});
......@@ -56,7 +58,7 @@ describe('content_editor/services/content_editor', () => {
});
it('emits loadingError event', async () => {
contentEditor.on(LOADING_ERROR_EVENT, (e) => {
eventHub.$on(LOADING_ERROR_EVENT, (e) => {
expect(e).toBe('error');
});
......
......@@ -8,7 +8,6 @@ describe('Job log controllers', () => {
afterEach(() => {
if (wrapper?.destroy) {
wrapper.destroy();
wrapper = null;
}
});
......@@ -34,7 +33,6 @@ describe('Job log controllers', () => {
const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]');
const findRawLink = () => wrapper.find('[data-testid="raw-link"]');
const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
const findEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
......@@ -76,28 +74,6 @@ describe('Job log controllers', () => {
expect(findRawLinkController().exists()).toBe(false);
});
});
describe('when is erasable', () => {
beforeEach(() => {
createWrapper();
});
it('renders erase job link', () => {
expect(findEraseLink().exists()).toBe(true);
});
});
describe('when it is not erasable', () => {
beforeEach(() => {
createWrapper({
erasePath: null,
});
});
it('does not render erase button', () => {
expect(findEraseLink().exists()).toBe(false);
});
});
});
describe('scroll buttons', () => {
......
import { GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
import createStore from '~/jobs/store';
import job from '../mock_data';
......@@ -9,12 +8,12 @@ describe('Job Sidebar Retry Button', () => {
let wrapper;
const forwardDeploymentFailure = 'forward_deployment_failure';
const findRetryButton = () => wrapper.find(GlButton);
const findRetryLink = () => wrapper.find(GlLink);
const findRetryButton = () => wrapper.findByTestId('retry-job-button');
const findRetryLink = () => wrapper.findByTestId('retry-job-link');
const createWrapper = ({ props = {} } = {}) => {
store = createStore();
wrapper = shallowMount(JobsSidebarRetryButton, {
wrapper = shallowMountExtended(JobsSidebarRetryButton, {
propsData: {
href: job.retry_path,
modalId: 'modal-id',
......@@ -27,7 +26,6 @@ describe('Job Sidebar Retry Button', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
......@@ -44,7 +42,6 @@ describe('Job Sidebar Retry Button', () => {
expect(findRetryButton().exists()).toBe(buttonExists);
expect(findRetryLink().exists()).toBe(linkExists);
expect(wrapper.text()).toMatch('Retry');
},
);
......@@ -55,6 +52,7 @@ describe('Job Sidebar Retry Button', () => {
expect(findRetryButton().attributes()).toMatchObject({
category: 'primary',
variant: 'confirm',
icon: 'retry',
});
});
});
......@@ -64,6 +62,7 @@ describe('Job Sidebar Retry Button', () => {
expect(findRetryLink().attributes()).toMatchObject({
'data-method': 'post',
href: job.retry_path,
icon: 'retry',
});
});
});
......
......@@ -21,25 +21,54 @@ describe('Sidebar details block', () => {
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
const findRetryButton = () => wrapper.find(JobRetryButton);
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
const findEraseLink = () => wrapper.findByTestId('job-log-erase-link');
const createWrapper = ({ props = {} } = {}) => {
const createWrapper = (props) => {
store = createStore();
store.state.job = job;
wrapper = extendedWrapper(
shallowMount(Sidebar, {
...props,
propsData: {
...props,
},
store,
}),
);
};
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
wrapper.destroy();
});
describe('when job log is erasable', () => {
const path = '/root/ci-project/-/jobs/1447/erase';
beforeEach(() => {
createWrapper({
erasePath: path,
});
});
it('renders erase job link', () => {
expect(findEraseLink().exists()).toBe(true);
});
it('erase job link has correct path', () => {
expect(findEraseLink().attributes('href')).toBe(path);
});
});
describe('when job log is not erasable', () => {
beforeEach(() => {
createWrapper();
});
it('does not render erase button', () => {
expect(findEraseLink().exists()).toBe(false);
});
});
describe('when there is no retry path retry', () => {
......@@ -86,7 +115,7 @@ describe('Sidebar details block', () => {
});
it('should render link to cancel job', () => {
expect(findCancelButton().text()).toMatch('Cancel');
expect(findCancelButton().props('icon')).toBe('cancel');
expect(findCancelButton().attributes('href')).toBe(job.cancel_path);
});
});
......
......@@ -32,12 +32,21 @@ RSpec.describe Gitlab::Email::Receiver do
metadata = receiver.mail_metadata
expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta))
expect(metadata.keys).to match_array(%i(mail_uid from_address to_address mail_key references delivered_to envelope_to x_envelope_to meta received_recipients))
expect(metadata[:meta]).to include(client_id: 'email/jake@example.com', project: project.full_path)
expect(metadata[meta_key]).to eq(meta_value)
end
end
shared_examples 'failed receive' do
it 'adds metric event' do
expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name })
expect { receiver.execute }.to raise_error(expected_error)
end
end
context 'when the email contains a valid email address in a header' do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
......@@ -74,14 +83,25 @@ RSpec.describe Gitlab::Email::Receiver do
it_behaves_like 'successful receive'
end
end
shared_examples 'failed receive' do
it 'adds metric event' do
expect(::Gitlab::Metrics::BackgroundTransaction).to receive(:current).and_return(metric_transaction)
expect(metric_transaction).to receive(:add_event).with('email_receiver_error', { error: expected_error.name })
context 'when all other headers are missing' do
let(:email_raw) { fixture_file('emails/missing_delivered_to_header.eml') }
let(:meta_key) { :received_recipients }
let(:meta_value) { ['incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com', 'incoming+gitlabhq/gitlabhq@example.com'] }
expect { receiver.execute }.to raise_error(expected_error)
context 'when use_received_header_for_incoming_emails is enabled' do
it_behaves_like 'successful receive'
end
context 'when use_received_header_for_incoming_emails is disabled' do
let(:expected_error) { Gitlab::Email::UnknownIncomingEmail }
before do
stub_feature_flags(use_received_header_for_incoming_emails: false)
end
it_behaves_like 'failed receive'
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment