Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4b1fc3dc32
commit
6619ed911f
|
@ -726,6 +726,7 @@ Gitlab/NamespacedClass:
|
|||
- 'app/validators/top_level_group_validator.rb'
|
||||
- 'app/validators/untrusted_regexp_validator.rb'
|
||||
- 'app/validators/x509_certificate_credentials_validator.rb'
|
||||
- 'app/validators/bytesize_validator.rb'
|
||||
- 'app/workers/admin_email_worker.rb'
|
||||
- 'app/workers/approve_blocked_pending_approval_users_worker.rb'
|
||||
- 'app/workers/archive_trace_worker.rb'
|
||||
|
|
67
CHANGELOG.md
67
CHANGELOG.md
|
@ -2,6 +2,28 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 15.3.2 (2022-08-30)
|
||||
|
||||
### Security (17 changes)
|
||||
|
||||
- [No overriding methods for Sawyer class](gitlab-org/security/gitlab@397aa9e269676f4ab3dfba4c3ba8fef131b5b4bd) ([merge request](gitlab-org/security/gitlab!2754))
|
||||
- [Update Oj to v3.13.21](gitlab-org/security/gitlab@15f86c00b579ad1b4aeedd395f9239e8229c6f8b) ([merge request](gitlab-org/security/gitlab!2730))
|
||||
- [Prevent long loops when generating suggested branch name](gitlab-org/security/gitlab@1479c9e2a0444794ea274b07e0f59e8a50ced6ee) ([merge request](gitlab-org/security/gitlab!2743))
|
||||
- [IDOR in Zentao integration issue show page](gitlab-org/security/gitlab@92fdf89045bf294d4ee0338ba3f26c91094a073e) ([merge request](gitlab-org/security/gitlab!2740))
|
||||
- [Patch VULNDB-255039 (potential Rack cache poisoning)](gitlab-org/security/gitlab@383c926cc8aa4e2c4273556a181e1ddc1b71049f) ([merge request](gitlab-org/security/gitlab!2697))
|
||||
- [HTML escape the label background color](gitlab-org/security/gitlab@1e43656560fbc13907af72d5d4f696df95d7f49c) ([merge request](gitlab-org/security/gitlab!2719))
|
||||
- [Sandbox jupyter notebook HTML output](gitlab-org/security/gitlab@3ade5f2fadbb0c15d9e5a14306d0a79136a8f23e) ([merge request](gitlab-org/security/gitlab!2710))
|
||||
- [Fix unauthorized GFM references in Incident Timeline](gitlab-org/security/gitlab@2e18b59472b5a43921d39433e60038b0f254d123) ([merge request](gitlab-org/security/gitlab!2707))
|
||||
- [Optimize handling repositories with huge trees](gitlab-org/security/gitlab@4bfaca71c8d8f663242138049cf5639e69326bbb) ([merge request](gitlab-org/security/gitlab!2706))
|
||||
- [Parse commit trailers without using regexp](gitlab-org/security/gitlab@c15b2cd9b5e572a9bbc7c0c5cb7c9511f1a04ead) ([merge request](gitlab-org/security/gitlab!2699))
|
||||
- [Check for pathological markdown input](gitlab-org/security/gitlab@2fd5e1133e1acd82cdb524f059b554976cd68f51) ([merge request](gitlab-org/security/gitlab!2733))
|
||||
- [Replaced smooshpack to fix the vulnerability in LivePreview](gitlab-org/security/gitlab@114637f8f0d9add00914ac3e4562419b0f1b4f63) ([merge request](gitlab-org/security/gitlab!2739))
|
||||
- [Update package auth for group IP allowlist](gitlab-org/security/gitlab@7e830349a8425dbab65ce92d3e8ebd0afa734381) ([merge request](gitlab-org/security/gitlab!2686))
|
||||
- [Don't show pipeline status](gitlab-org/security/gitlab@1b5fbb9bcb4dde12a2af075e45407cbc6109494d) ([merge request](gitlab-org/security/gitlab!2712))
|
||||
- [Sanitize img attributes in Banzai::Filter::ImageLinkFilter](gitlab-org/security/gitlab@22ece3568d6b3aed305ed97aab9fdbb22ca068e8) ([merge request](gitlab-org/security/gitlab!2722))
|
||||
- [Validate description length for snippets](gitlab-org/security/gitlab@24592d39d7b8956a0e712026e5b988a82d37e771) ([merge request](gitlab-org/security/gitlab!2702))
|
||||
- [Prevent brute force vuln for Git over HTTP(S) requests](gitlab-org/security/gitlab@fcff307eff525d15e835e65e0e3e3a2395f0b840) ([merge request](gitlab-org/security/gitlab!2716))
|
||||
|
||||
## 15.3.1 (2022-08-22)
|
||||
|
||||
### Security (1 change)
|
||||
|
@ -613,6 +635,29 @@ entry.
|
|||
- [Remove FF import_release_authors_from_github](gitlab-org/gitlab@c4d6871e4438a1626d688856903778623138f671) ([merge request](gitlab-org/gitlab!92686))
|
||||
- [Remove unused feature](gitlab-org/gitlab@0ef95d341e4a15150d6ccb3d104ebbe064aa062a) ([merge request](gitlab-org/gitlab!92753))
|
||||
|
||||
## 15.2.4 (2022-08-30)
|
||||
|
||||
### Security (18 changes)
|
||||
|
||||
- [No overriding methods for Sawyer class](gitlab-org/security/gitlab@fafcaf91c510ace5c3fc845197fa71d2ad8943cc) ([merge request](gitlab-org/security/gitlab!2755))
|
||||
- [Update Oj to v3.13.21](gitlab-org/security/gitlab@e14f62112f51315288f3f08108b59cf40ab5635e) ([merge request](gitlab-org/security/gitlab!2729))
|
||||
- [Bump yajl-ruby gem version](gitlab-org/security/gitlab@ad7469e802aff36989276bd77afcebf9bcb8a545) ([merge request](gitlab-org/security/gitlab!2689))
|
||||
- [Prevent long loops when generating suggested branch name](gitlab-org/security/gitlab@f8f1631a7751b40444debbd69188187c895d2ad6) ([merge request](gitlab-org/security/gitlab!2744))
|
||||
- [IDOR in Zentao integration issue show page](gitlab-org/security/gitlab@01004871400564e5b18a2efa4f6d87c8ca37db5c) ([merge request](gitlab-org/security/gitlab!2741))
|
||||
- [Patch VULNDB-255039 (potential Rack cache poisoning)](gitlab-org/security/gitlab@a951318f5870e8f35c742eab58132c63d6d36198) ([merge request](gitlab-org/security/gitlab!2694))
|
||||
- [HTML escape the label background color](gitlab-org/security/gitlab@de115e3b0896aa1504882d3230b5427506fee3e2) ([merge request](gitlab-org/security/gitlab!2720))
|
||||
- [Sandbox jupyter notebook HTML output](gitlab-org/security/gitlab@67aeba4ae4c95d2668d0428cb66d263ee4247b68) ([merge request](gitlab-org/security/gitlab!2711))
|
||||
- [Fix unauthorized GFM references in Incident Timeline](gitlab-org/security/gitlab@f091bc238efa1d669c1257aa146339f4b1134a0c) ([merge request](gitlab-org/security/gitlab!2708))
|
||||
- [Optimize handling repositories with huge trees](gitlab-org/security/gitlab@9969c2cabccef2367631498f38ab8d0b19cf9da3) ([merge request](gitlab-org/security/gitlab!2666))
|
||||
- [Parse commit trailers without using regexp](gitlab-org/security/gitlab@9bd64457525313a949f151fd27f2954ff71e399d) ([merge request](gitlab-org/security/gitlab!2700))
|
||||
- [Check for pathological markdown input](gitlab-org/security/gitlab@c05642874c38e4d914297ad788a07c42b77b6b1e) ([merge request](gitlab-org/security/gitlab!2732))
|
||||
- [Replaced smooshpack to fix the vulnerability in LivePreview](gitlab-org/security/gitlab@e48df65563c6c66fd6d89fb7bf626bdf8b465cc0) ([merge request](gitlab-org/security/gitlab!2662))
|
||||
- [Update package auth for group IP allowlist](gitlab-org/security/gitlab@eb7b9e646732cc3590e00d5694d5a662e71c9f99) ([merge request](gitlab-org/security/gitlab!2684))
|
||||
- [Don't show pipeline status](gitlab-org/security/gitlab@a5962d9ee7aec4f86a982f2d686a690806df6f15) ([merge request](gitlab-org/security/gitlab!2680))
|
||||
- [Sanitize img attributes in Banzai::Filter::ImageLinkFilter](gitlab-org/security/gitlab@ee68b29c2199e1c399a4d0065ed53c50592e54a0) ([merge request](gitlab-org/security/gitlab!2676))
|
||||
- [Validate description length for snippets](gitlab-org/security/gitlab@e9e4c3b3109590a5c12ecb2f25e4641dd408ce36) ([merge request](gitlab-org/security/gitlab!2703))
|
||||
- [Prevent brute force vuln for Git over HTTP(S) requests](gitlab-org/security/gitlab@aab24e532b8c0b9e8acc90e7954434519e19b908) ([merge request](gitlab-org/security/gitlab!2717))
|
||||
|
||||
## 15.2.3 (2022-08-22)
|
||||
|
||||
### Security (2 changes)
|
||||
|
@ -1336,6 +1381,28 @@ entry.
|
|||
- [Update GitLab Runner Helm Chart to 0.42.0](gitlab-org/gitlab@cc89200f498fe216864914c79b5b0d1d578edab3) ([merge request](gitlab-org/gitlab!90605))
|
||||
- [Address database documentation Vale warningss](gitlab-org/gitlab@e5f9a089766bace046d3bbd760a2979865a4bbc0) by @cgives ([merge request](gitlab-org/gitlab!90093))
|
||||
|
||||
## 15.1.6 (2022-08-30)
|
||||
|
||||
### Security (17 changes)
|
||||
|
||||
- [No overriding methods for Sawyer class](gitlab-org/security/gitlab@720a17d03791c298d193b2d49d322a5f259bb6f2) ([merge request](gitlab-org/security/gitlab!2756))
|
||||
- [Bump yajl-ruby gem version](gitlab-org/security/gitlab@acb8bee73354ddbd7a7a52e3d09c870d1cd99e27) ([merge request](gitlab-org/security/gitlab!2690))
|
||||
- [Prevent long loops when generating suggested branch name](gitlab-org/security/gitlab@e331ecf658de25901def2ea4a368104b82a0109c) ([merge request](gitlab-org/security/gitlab!2745))
|
||||
- [IDOR in Zentao integration issue show page](gitlab-org/security/gitlab@0a238baf6a1d4aa0bc834448aefaf756d594a7be) ([merge request](gitlab-org/security/gitlab!2742))
|
||||
- [Patch VULNDB-255039 (potential Rack cache poisoning)](gitlab-org/security/gitlab@1f5ecd95b3631c8352ff57cf4bee23d26aa51ecc) ([merge request](gitlab-org/security/gitlab!2695))
|
||||
- [HTML escape the label background color](gitlab-org/security/gitlab@470b75a53ea4383ea30de5a482d39b322f87dfa2) ([merge request](gitlab-org/security/gitlab!2721))
|
||||
- [Sandbox jupyter notebook HTML output](gitlab-org/security/gitlab@72089898a60de7f17c19a2fa9d4f1330d3052b52) ([merge request](gitlab-org/security/gitlab!2713))
|
||||
- [Fix unauthorized GFM references in Incident Timeline](gitlab-org/security/gitlab@c62408682ed47bc2e5f93585a5b4e92e8cfebf9f) ([merge request](gitlab-org/security/gitlab!2709))
|
||||
- [Optimize handling repositories with huge trees](gitlab-org/security/gitlab@396f20e019a9888d1645e9345a82fdf21153bf76) ([merge request](gitlab-org/security/gitlab!2667))
|
||||
- [Parse commit trailers without using regexp](gitlab-org/security/gitlab@b377a1ecbb37c5359b2c2a0ecfbd911654664700) ([merge request](gitlab-org/security/gitlab!2701))
|
||||
- [Check for pathological markdown input](gitlab-org/security/gitlab@e3a1376ec70d8d60f11a380cce6e0b3c35f68646) ([merge request](gitlab-org/security/gitlab!2731))
|
||||
- [Replaced smooshpack to fix the vulnerability in LivePreview](gitlab-org/security/gitlab@d520ffd2a5a75d33ac98c39cd2f2fe623b0e1115) ([merge request](gitlab-org/security/gitlab!2664))
|
||||
- [Update package auth for group IP allowlist](gitlab-org/security/gitlab@12bb8656bdaa9a7502c0a1b77c12fefb72677ba1) ([merge request](gitlab-org/security/gitlab!2685))
|
||||
- [Don't show pipeline status](gitlab-org/security/gitlab@7fb43f899f2342704bda81643f8375a126efc2ae) ([merge request](gitlab-org/security/gitlab!2679))
|
||||
- [Sanitize img attributes in Banzai::Filter::ImageLinkFilter](gitlab-org/security/gitlab@594fa5874fb7cc6b6588bbf8aff2f04b8acbbfd0) ([merge request](gitlab-org/security/gitlab!2677))
|
||||
- [Validate description length for snippets](gitlab-org/security/gitlab@94ae3d05741bc69b9307e5f58f0d61bf2566c21b) ([merge request](gitlab-org/security/gitlab!2704))
|
||||
- [Prevent brute force vuln for Git over HTTP(S) requests](gitlab-org/security/gitlab@7b76542e197ea72289c881c312b3a519c8b28e63) ([merge request](gitlab-org/security/gitlab!2718))
|
||||
|
||||
## 15.1.5 (2022-08-22)
|
||||
|
||||
### Security (2 changes)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { listen } from 'codesandbox-api';
|
||||
import { isEmpty, debounce } from 'lodash';
|
||||
import { Manager } from 'smooshpack';
|
||||
import { SandpackClient } from '@codesandbox/sandpack-client';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import {
|
||||
packageJsonPath,
|
||||
|
@ -21,7 +21,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
manager: {},
|
||||
client: {},
|
||||
loading: false,
|
||||
sandpackReady: false,
|
||||
};
|
||||
|
@ -94,11 +94,11 @@ export default {
|
|||
this.sandpackReady = false;
|
||||
eventHub.$off('ide.files.change', this.onFilesChangeCallback);
|
||||
|
||||
if (!isEmpty(this.manager)) {
|
||||
this.manager.listener();
|
||||
if (!isEmpty(this.client)) {
|
||||
this.client.cleanup();
|
||||
}
|
||||
|
||||
this.manager = {};
|
||||
this.client = {};
|
||||
|
||||
if (this.listener) {
|
||||
this.listener();
|
||||
|
@ -120,7 +120,7 @@ export default {
|
|||
return this.loadFileContent(this.mainEntry)
|
||||
.then(() => this.$nextTick())
|
||||
.then(() => {
|
||||
this.initManager();
|
||||
this.initClient();
|
||||
|
||||
this.listener = listen((e) => {
|
||||
switch (e.type) {
|
||||
|
@ -136,15 +136,15 @@ export default {
|
|||
update() {
|
||||
if (!this.sandpackReady) return;
|
||||
|
||||
if (isEmpty(this.manager)) {
|
||||
if (isEmpty(this.client)) {
|
||||
this.initPreview();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.manager.updatePreview(this.sandboxOpts);
|
||||
this.client.updatePreview(this.sandboxOpts);
|
||||
},
|
||||
initManager() {
|
||||
initClient() {
|
||||
const { codesandboxBundlerUrl: bundlerURL } = this;
|
||||
|
||||
const settings = {
|
||||
|
@ -155,7 +155,7 @@ export default {
|
|||
...(bundlerURL ? { bundlerURL } : {}),
|
||||
};
|
||||
|
||||
this.manager = new Manager('#ide-preview', this.sandboxOpts, settings);
|
||||
this.client = new SandpackClient('#ide-preview', this.sandboxOpts, settings);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -164,7 +164,7 @@ export default {
|
|||
<template>
|
||||
<div class="preview h-100 w-100 d-flex flex-column gl-bg-white">
|
||||
<template v-if="showPreview">
|
||||
<navigator :manager="manager" />
|
||||
<navigator :client="client" />
|
||||
<div id="ide-preview"></div>
|
||||
</template>
|
||||
<div
|
||||
|
|
|
@ -8,7 +8,7 @@ export default {
|
|||
GlLoadingIcon,
|
||||
},
|
||||
props: {
|
||||
manager: {
|
||||
client: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
@ -51,7 +51,7 @@ export default {
|
|||
onUrlChange(e) {
|
||||
const lastPath = this.path;
|
||||
|
||||
this.path = e.url.replace(this.manager.bundlerURL, '') || '/';
|
||||
this.path = e.url.replace(this.client.bundlerURL, '') || '/';
|
||||
|
||||
if (lastPath !== this.path) {
|
||||
this.currentBrowsingIndex =
|
||||
|
@ -79,7 +79,7 @@ export default {
|
|||
},
|
||||
visitPath(path) {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
this.manager.iframe.src = `${this.manager.bundlerURL}${path}`;
|
||||
this.client.iframe.src = `${this.client.bundlerURL}${path}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -26,4 +26,8 @@ export const timelineListI18n = Object.freeze({
|
|||
'Incident|Something went wrong while deleting the incident timeline event.',
|
||||
),
|
||||
deleteModal: s__('Incident|Are you sure you want to delete this event?'),
|
||||
editError: s__('Incident|Error updating incident timeline event: %{error}'),
|
||||
editErrorGeneric: s__(
|
||||
'Incident|Something went wrong while updating the incident timeline event.',
|
||||
),
|
||||
});
|
||||
|
|
|
@ -33,9 +33,6 @@ export default {
|
|||
clearForm() {
|
||||
this.$refs.eventForm.clear();
|
||||
},
|
||||
focusDate() {
|
||||
this.$refs.eventForm.focusDate();
|
||||
},
|
||||
updateCache(store, { data }) {
|
||||
const { timelineEvent: event, errors } = data?.timelineEventCreate || {};
|
||||
|
||||
|
@ -110,7 +107,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<div
|
||||
class="gl-relative gl-display-flex gl-align-items-center"
|
||||
class="create-timeline-event gl-relative gl-display-flex gl-align-items-start"
|
||||
:class="{ 'timeline-entry-vertical-line': hasTimelineEvents }"
|
||||
>
|
||||
<div
|
||||
|
@ -121,8 +118,9 @@ export default {
|
|||
</div>
|
||||
<timeline-events-form
|
||||
ref="eventForm"
|
||||
:class="{ 'gl-border-gray-50 gl-border-t': hasTimelineEvents }"
|
||||
:is-event-processed="createTimelineEventActive"
|
||||
:has-timeline-events="hasTimelineEvents"
|
||||
show-save-and-add
|
||||
@save-event="createIncidentTimelineEvent"
|
||||
@cancel="$emit('hide-new-timeline-events-form')"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import TimelineEventsForm from './timeline_events_form.vue';
|
||||
|
||||
export default {
|
||||
name: 'EditTimelineEvent',
|
||||
components: {
|
||||
TimelineEventsForm,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
event: {
|
||||
type: Object,
|
||||
required: true,
|
||||
validator: (item) => ['occurredAt', 'note'].every((key) => item[key]),
|
||||
},
|
||||
editTimelineEventActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveEvent(eventDetails) {
|
||||
this.$emit('handle-save-edit', { ...eventDetails, id: this.event.id }, false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-relative gl-display-flex gl-align-items-center">
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-align-self-start gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-mr-3 gl-w-8 gl-h-8 gl-z-index-1"
|
||||
>
|
||||
<gl-icon name="comment" class="note-icon" />
|
||||
</div>
|
||||
<timeline-events-form
|
||||
ref="eventForm"
|
||||
class="timeline-event-border"
|
||||
:is-event-processed="editTimelineEventActive"
|
||||
:previous-occurred-at="event.occurredAt"
|
||||
:previous-note="event.note"
|
||||
@save-event="saveEvent"
|
||||
@cancel="$emit('hide-edit')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,13 @@
|
|||
mutation UpdateTimelineEvent($input: TimelineEventUpdateInput!) {
|
||||
timelineEventUpdate(input: $input) {
|
||||
timelineEvent {
|
||||
id
|
||||
note
|
||||
noteHtml
|
||||
action
|
||||
occurredAt
|
||||
createdAt
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
|
|||
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
import { timelineFormI18n } from './constants';
|
||||
import { getUtcShiftedDateNow } from './utils';
|
||||
import { getUtcShiftedDate } from './utils';
|
||||
|
||||
export default {
|
||||
name: 'TimelineEventsForm',
|
||||
|
@ -29,21 +29,32 @@ export default {
|
|||
autofocusonshow,
|
||||
},
|
||||
props: {
|
||||
hasTimelineEvents: {
|
||||
showSaveAndAdd: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isEventProcessed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
previousOccurredAt: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
previousNote: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
// if occurredAt is undefined, returns "now" in UTC
|
||||
const placeholderDate = getUtcShiftedDateNow();
|
||||
// if occurredAt is null, returns "now" in UTC
|
||||
const placeholderDate = getUtcShiftedDate(this.previousOccurredAt);
|
||||
|
||||
return {
|
||||
timelineText: '',
|
||||
timelineText: this.previousNote,
|
||||
placeholderDate,
|
||||
hourPickerInput: placeholderDate.getHours(),
|
||||
minutePickerInput: placeholderDate.getMinutes(),
|
||||
|
@ -51,7 +62,7 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
occurredAt() {
|
||||
occurredAtString() {
|
||||
const year = this.datePickerInput.getFullYear();
|
||||
const month = this.datePickerInput.getMonth();
|
||||
const day = this.datePickerInput.getDate();
|
||||
|
@ -63,38 +74,36 @@ export default {
|
|||
return utcDate.toISOString();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.focusDate();
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
const newPlaceholderDate = getUtcShiftedDateNow();
|
||||
const newPlaceholderDate = getUtcShiftedDate();
|
||||
this.datePickerInput = newPlaceholderDate;
|
||||
this.hourPickerInput = newPlaceholderDate.getHours();
|
||||
this.minutePickerInput = newPlaceholderDate.getMinutes();
|
||||
this.timelineText = '';
|
||||
},
|
||||
focusDate() {
|
||||
this.$refs.datepicker.$el.focus();
|
||||
this.$refs.datepicker.$el.querySelector('input').focus();
|
||||
},
|
||||
handleSave(addAnotherEvent) {
|
||||
const eventDetails = {
|
||||
const event = {
|
||||
note: this.timelineText,
|
||||
occurredAt: this.occurredAt,
|
||||
occurredAt: this.occurredAtString,
|
||||
};
|
||||
this.$emit('save-event', eventDetails, addAnotherEvent);
|
||||
this.$emit('save-event', event, addAnotherEvent);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form class="gl-flex-grow-1 gl-border-gray-50" :class="{ 'gl-border-t': hasTimelineEvents }">
|
||||
<form class="gl-flex-grow-1 gl-border-gray-50">
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row">
|
||||
<gl-form-group :label="__('Date')" class="gl-mt-5 gl-mr-5">
|
||||
<gl-datepicker
|
||||
id="incident-date"
|
||||
ref="datepicker"
|
||||
v-model="datePickerInput"
|
||||
data-testid="input-datepicker"
|
||||
/>
|
||||
<gl-datepicker id="incident-date" ref="datepicker" v-model="datePickerInput" />
|
||||
</gl-form-group>
|
||||
<div class="gl-display-flex gl-mt-5">
|
||||
<gl-form-group :label="__('Time')">
|
||||
|
@ -163,6 +172,7 @@ export default {
|
|||
{{ $options.i18n.save }}
|
||||
</gl-button>
|
||||
<gl-button
|
||||
v-if="showSaveAndAdd"
|
||||
variant="confirm"
|
||||
category="secondary"
|
||||
class="gl-mr-3 gl-ml-n2"
|
||||
|
@ -174,7 +184,7 @@ export default {
|
|||
<gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
|
||||
{{ $options.i18n.cancel }}
|
||||
</gl-button>
|
||||
<div class="gl-border-b gl-pt-5"></div>
|
||||
<div class="timeline-event-bottom-border"></div>
|
||||
</gl-form-group>
|
||||
</form>
|
||||
</template>
|
||||
|
|
|
@ -8,6 +8,7 @@ export default {
|
|||
name: 'IncidentTimelineEventListItem',
|
||||
i18n: {
|
||||
delete: __('Delete'),
|
||||
edit: __('Edit'),
|
||||
moreActions: __('More actions'),
|
||||
timeUTC: __('%{time} UTC'),
|
||||
},
|
||||
|
@ -22,10 +23,6 @@ export default {
|
|||
},
|
||||
inject: ['canUpdate'],
|
||||
props: {
|
||||
isLastItem: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
occurredAt: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -50,15 +47,14 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<div class="gl-display-flex gl-align-items-start">
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-n2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
|
||||
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
|
||||
>
|
||||
<gl-icon :name="getEventIcon(action)" class="note-icon" />
|
||||
</div>
|
||||
<div
|
||||
class="timeline-event-note gl-w-full gl-display-flex gl-flex-direction-row"
|
||||
:class="{ 'gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid': !isLastItem }"
|
||||
class="timeline-event-note timeline-event-border gl-w-full gl-display-flex gl-flex-direction-row"
|
||||
data-testid="event-text-container"
|
||||
>
|
||||
<div>
|
||||
|
@ -72,13 +68,16 @@ export default {
|
|||
<gl-dropdown
|
||||
v-if="canUpdate"
|
||||
right
|
||||
class="event-note-actions gl-ml-auto gl-align-self-center"
|
||||
class="event-note-actions gl-ml-auto gl-align-self-start"
|
||||
icon="ellipsis_v"
|
||||
text-sr-only
|
||||
:text="$options.i18n.moreActions"
|
||||
category="tertiary"
|
||||
no-caret
|
||||
>
|
||||
<gl-dropdown-item @click="$emit('edit')">
|
||||
{{ $options.i18n.edit }}
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item @click="$emit('delete')">
|
||||
{{ $options.i18n.delete }}
|
||||
</gl-dropdown-item>
|
||||
|
|
|
@ -5,7 +5,9 @@ import { sprintf } from '~/locale';
|
|||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
|
||||
import IncidentTimelineEventItem from './timeline_events_item.vue';
|
||||
import EditTimelineEvent from './edit_timeline_event.vue';
|
||||
import deleteTimelineEvent from './graphql/queries/delete_timeline_event.mutation.graphql';
|
||||
import editTimelineEvent from './graphql/queries/edit_timeline_event.mutation.graphql';
|
||||
import { timelineListI18n } from './constants';
|
||||
|
||||
export default {
|
||||
|
@ -13,6 +15,7 @@ export default {
|
|||
i18n: timelineListI18n,
|
||||
components: {
|
||||
IncidentTimelineEventItem,
|
||||
EditTimelineEvent,
|
||||
},
|
||||
props: {
|
||||
timelineEventLoading: {
|
||||
|
@ -26,6 +29,9 @@ export default {
|
|||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { eventToEdit: null, editTimelineEventActive: false };
|
||||
},
|
||||
computed: {
|
||||
dateGroupedEvents() {
|
||||
const groupedEvents = new Map();
|
||||
|
@ -44,11 +50,12 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
isLastItem(groups, groupIndex, events, eventIndex) {
|
||||
if (groupIndex < groups.size - 1) {
|
||||
return false;
|
||||
}
|
||||
return eventIndex === events.length - 1;
|
||||
handleEditSelection(event) {
|
||||
this.eventToEdit = event.id;
|
||||
this.$emit('hide-new-incident-timeline-event-form');
|
||||
},
|
||||
hideEdit() {
|
||||
this.eventToEdit = null;
|
||||
},
|
||||
handleDelete: ignoreWhilePending(async function handleDelete(event) {
|
||||
const msg = this.$options.i18n.deleteModal;
|
||||
|
@ -85,6 +92,38 @@ export default {
|
|||
createAlert({ message: this.$options.i18n.deleteErrorGeneric, captureError: true, error });
|
||||
}
|
||||
}),
|
||||
handleSaveEdit(eventDetails) {
|
||||
this.editTimelineEventActive = true;
|
||||
return this.$apollo
|
||||
.mutate({
|
||||
mutation: editTimelineEvent,
|
||||
variables: {
|
||||
input: {
|
||||
id: eventDetails.id,
|
||||
note: eventDetails.note,
|
||||
occurredAt: eventDetails.occurredAt,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
this.editTimelineEventActive = false;
|
||||
const errors = data.timelineEventUpdate?.errors;
|
||||
if (errors.length) {
|
||||
createAlert({
|
||||
message: sprintf(this.$options.i18n.editError, { error: errors.join('. ') }, false),
|
||||
});
|
||||
} else {
|
||||
this.hideEdit();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
createAlert({
|
||||
message: this.$options.i18n.editErrorGeneric,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -92,9 +131,10 @@ export default {
|
|||
<template>
|
||||
<div class="issuable-discussion incident-timeline-events">
|
||||
<div
|
||||
v-for="([eventDate, events], groupIndex) in dateGroupedEvents"
|
||||
v-for="[eventDate, events] in dateGroupedEvents"
|
||||
:key="eventDate"
|
||||
data-testid="timeline-group"
|
||||
class="timeline-group"
|
||||
>
|
||||
<div class="gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid">
|
||||
<strong class="gl-font-size-h2" data-testid="event-date">{{ eventDate }}</strong>
|
||||
|
@ -103,15 +143,25 @@ export default {
|
|||
<li
|
||||
v-for="(event, eventIndex) in events"
|
||||
:key="eventIndex"
|
||||
class="timeline-entry-vertical-line note system-note note-wrapper gl-my-2! gl-pr-0!"
|
||||
class="timeline-entry-vertical-line timeline-entry note system-note note-wrapper gl-my-2! gl-pr-0!"
|
||||
>
|
||||
<edit-timeline-event
|
||||
v-if="eventToEdit === event.id"
|
||||
:key="`edit-${event.id}`"
|
||||
ref="eventForm"
|
||||
:event="event"
|
||||
:edit-timeline-event-active="editTimelineEventActive"
|
||||
@handle-save-edit="handleSaveEdit"
|
||||
@hide-edit="hideEdit()"
|
||||
/>
|
||||
<incident-timeline-event-item
|
||||
v-else
|
||||
:key="event.id"
|
||||
:action="event.action"
|
||||
:occurred-at="event.occurredAt"
|
||||
:note-html="event.noteHtml"
|
||||
:is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
|
||||
@delete="handleDelete(event)"
|
||||
@edit="handleEditSelection(event)"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -69,11 +69,8 @@ export default {
|
|||
hideEventForm() {
|
||||
this.isEventFormVisible = false;
|
||||
},
|
||||
async showEventForm() {
|
||||
this.$refs.createEventForm.clearForm();
|
||||
showEventForm() {
|
||||
this.isEventFormVisible = true;
|
||||
await this.$nextTick();
|
||||
this.$refs.createEventForm.focusDate();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -94,7 +91,7 @@ export default {
|
|||
@hide-new-timeline-events-form="hideEventForm"
|
||||
/>
|
||||
<create-timeline-event
|
||||
v-show="isEventFormVisible"
|
||||
v-if="isEventFormVisible"
|
||||
ref="createEventForm"
|
||||
:has-timeline-events="hasTimelineEvents"
|
||||
class="timeline-event-note timeline-event-note-form"
|
||||
|
|
|
@ -21,13 +21,14 @@ export const getEventIcon = (actionName) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Returns a date shifted by the current timezone offset. Allows
|
||||
* date.getHours() and similar to return UTC values.
|
||||
*
|
||||
* Returns a date shifted by the current timezone offset set to now
|
||||
* by default but can accept an existing date as an ISO date string
|
||||
* @param {string} ISOString
|
||||
* @returns {Date}
|
||||
*/
|
||||
export const getUtcShiftedDateNow = () => {
|
||||
const date = new Date();
|
||||
export const getUtcShiftedDate = (ISOString = null) => {
|
||||
const date = ISOString ? new Date(ISOString) : new Date();
|
||||
date.setMinutes(date.getMinutes() + date.getTimezoneOffset());
|
||||
|
||||
return date;
|
||||
};
|
||||
|
|
|
@ -40,6 +40,13 @@ export default {
|
|||
<template>
|
||||
<div class="output">
|
||||
<prompt type="Out" :count="count" :show-output="showOutput" />
|
||||
<div v-safe-html:[$options.safeHtmlConfig]="rawCode" class="gl-overflow-auto"></div>
|
||||
<iframe
|
||||
sandbox
|
||||
:srcdoc="rawCode"
|
||||
frameborder="0"
|
||||
scrolling="no"
|
||||
width="100%"
|
||||
class="gl-overflow-auto"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
|
||||
a,
|
||||
button {
|
||||
padding: $gl-padding-8;
|
||||
font-size: 14px;
|
||||
line-height: 28px;
|
||||
padding: $gl-spacing-scale-5 $gl-spacing-scale-4;
|
||||
font-size: $gl-font-size;
|
||||
line-height: $gl-line-height-16;
|
||||
color: $gl-text-color-secondary;
|
||||
border: 0;
|
||||
white-space: nowrap;
|
||||
|
@ -88,10 +88,6 @@
|
|||
float: left;
|
||||
}
|
||||
|
||||
li a {
|
||||
padding: 16px 15px 11px;
|
||||
}
|
||||
|
||||
/* Small devices (phones, 768px and lower) */
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 100%;
|
||||
|
|
|
@ -962,40 +962,26 @@
|
|||
border-left: 2px solid $gray-50;
|
||||
position: absolute;
|
||||
left: 39px;
|
||||
height: 80%;
|
||||
height: calc(100% + #{$gl-spacing-scale-5});
|
||||
top: -#{$gl-spacing-scale-5};
|
||||
}
|
||||
|
||||
&:first-child::before,
|
||||
&:last-child::after {
|
||||
&:first-child::before {
|
||||
content: none;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&::after {
|
||||
top: 50%;
|
||||
top: $gl-spacing-scale-5;
|
||||
height: calc(100% + #{$gl-spacing-scale-5});
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:last-child,
|
||||
&.create-timeline-event {
|
||||
&::before {
|
||||
bottom: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
&::before {
|
||||
top: -10%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: -10%;
|
||||
}
|
||||
}
|
||||
|
||||
&.timeline-event-note-form {
|
||||
&::before {
|
||||
top: -15% !important; // Override default positioning
|
||||
height: 20%;
|
||||
top: - #{$gl-spacing-scale-5} !important; // Override default positioning
|
||||
@include gl-h-8;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
@ -1007,3 +993,22 @@
|
|||
.timeline-event-note-form {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.timeline-entry:not(:last-child) {
|
||||
.timeline-event-border {
|
||||
@include gl-pb-5;
|
||||
@include gl-border-gray-50;
|
||||
@include gl-border-1;
|
||||
@include gl-border-b-solid;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-group:last-child {
|
||||
.timeline-entry:last-child,
|
||||
.create-timeline-event {
|
||||
.timeline-event-bottom-border {
|
||||
@include gl-border-b;
|
||||
@include gl-pt-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,22 +37,11 @@ class JwtController < ApplicationController
|
|||
|
||||
if @authentication_result.failed?
|
||||
log_authentication_failed(login, @authentication_result)
|
||||
render_unauthorized
|
||||
render_access_denied
|
||||
end
|
||||
end
|
||||
rescue Gitlab::Auth::MissingPersonalAccessTokenError
|
||||
render_missing_personal_access_token
|
||||
end
|
||||
|
||||
def render_missing_personal_access_token
|
||||
render json: {
|
||||
errors: [
|
||||
{ code: 'UNAUTHORIZED',
|
||||
message: _('HTTP Basic: Access denied\n' \
|
||||
'You must use a personal access token with \'api\' scope for Git over HTTP.\n' \
|
||||
'You can generate one at %{profile_personal_access_tokens_url}') % { profile_personal_access_tokens_url: profile_personal_access_tokens_url } }
|
||||
]
|
||||
}, status: :unauthorized
|
||||
render_access_denied
|
||||
end
|
||||
|
||||
def log_authentication_failed(login, result)
|
||||
|
@ -68,13 +57,19 @@ class JwtController < ApplicationController
|
|||
Gitlab::AuthLogger.warn(log_info)
|
||||
end
|
||||
|
||||
def render_unauthorized
|
||||
render json: {
|
||||
errors: [
|
||||
{ code: 'UNAUTHORIZED',
|
||||
message: 'HTTP Basic: Access denied' }
|
||||
]
|
||||
}, status: :unauthorized
|
||||
def render_access_denied
|
||||
help_page = help_page_url(
|
||||
'user/profile/account/two_factor_authentication',
|
||||
anchor: 'troubleshooting'
|
||||
)
|
||||
|
||||
render(
|
||||
json: { errors: [{
|
||||
code: 'UNAUTHORIZED',
|
||||
message: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page)
|
||||
}] },
|
||||
status: :unauthorized
|
||||
)
|
||||
end
|
||||
|
||||
def auth_params
|
||||
|
|
|
@ -67,9 +67,21 @@ module Repositories
|
|||
end
|
||||
|
||||
send_challenges
|
||||
render plain: "HTTP Basic: Access denied\n", status: :unauthorized
|
||||
render_access_denied
|
||||
rescue Gitlab::Auth::MissingPersonalAccessTokenError
|
||||
render_missing_personal_access_token
|
||||
render_access_denied
|
||||
end
|
||||
|
||||
def render_access_denied
|
||||
help_page = help_page_url(
|
||||
'topics/git/troubleshooting_git',
|
||||
anchor: 'error-on-git-fetch-http-basic-access-denied'
|
||||
)
|
||||
|
||||
render(
|
||||
plain: format(_("HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"), help_page_url: help_page),
|
||||
status: :unauthorized
|
||||
)
|
||||
end
|
||||
|
||||
def basic_auth_provided?
|
||||
|
@ -103,13 +115,6 @@ module Repositories
|
|||
@container, @project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(repository_path)
|
||||
end
|
||||
|
||||
def render_missing_personal_access_token
|
||||
render plain: "HTTP Basic: Access denied\n" \
|
||||
"You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \
|
||||
"You can generate one at #{profile_personal_access_tokens_url}",
|
||||
status: :unauthorized
|
||||
end
|
||||
|
||||
def repository
|
||||
strong_memoize(:repository) do
|
||||
repo_type.repository_for(container)
|
||||
|
|
|
@ -32,7 +32,11 @@ module Resolvers
|
|||
page_token: cursor
|
||||
}
|
||||
|
||||
tree = repository.tree(args[:ref], args[:path], recursive: args[:recursive], pagination_params: pagination_params)
|
||||
tree = repository.tree(
|
||||
args[:ref], args[:path], recursive: args[:recursive],
|
||||
skip_flat_paths: false,
|
||||
pagination_params: pagination_params
|
||||
)
|
||||
|
||||
next_cursor = tree.cursor&.next_cursor
|
||||
Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)
|
||||
|
|
|
@ -33,11 +33,6 @@ module Types
|
|||
null: true,
|
||||
description: 'Text note of the timeline event.'
|
||||
|
||||
field :note_html,
|
||||
GraphQL::Types::String,
|
||||
null: true,
|
||||
description: 'HTML note of the timeline event.'
|
||||
|
||||
field :promoted_from_note,
|
||||
Types::Notes::NoteType,
|
||||
null: true,
|
||||
|
@ -67,6 +62,8 @@ module Types
|
|||
Types::TimeType,
|
||||
null: false,
|
||||
description: 'Timestamp when the event updated.'
|
||||
|
||||
markdown_field :note_html, null: true, description: 'HTML note of the timeline event.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,7 +171,7 @@ module CommitsHelper
|
|||
ref,
|
||||
{
|
||||
merge_request: merge_request&.cache_key,
|
||||
pipeline_status: commit.status_for(ref)&.cache_key,
|
||||
pipeline_status: commit.detailed_status_for(ref)&.cache_key,
|
||||
xhr: request.xhr?,
|
||||
controller: controller.controller_path,
|
||||
path: @path # referred to in #link_to_browse_code
|
||||
|
|
|
@ -247,7 +247,7 @@ module LabelsHelper
|
|||
class="#{css_class}"
|
||||
data-container="body"
|
||||
data-html="true"
|
||||
#{"style=\"background-color: #{bg_color}\"" if bg_color}
|
||||
#{"style=\"background-color: #{h bg_color}\"" if bg_color}
|
||||
>#{ERB::Util.html_escape_once(name)}#{suffix}</span>
|
||||
HTML
|
||||
end
|
||||
|
|
|
@ -69,6 +69,10 @@ module Integrations
|
|||
}
|
||||
end
|
||||
|
||||
def client_url
|
||||
api_url.presence || url
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
name.demodulize.downcase
|
||||
end
|
||||
|
|
|
@ -448,7 +448,13 @@ class Issue < ApplicationRecord
|
|||
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
|
||||
|
||||
start_counting_from = 2
|
||||
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
|
||||
|
||||
branch_name_generator = -> (counter) do
|
||||
suffix = counter > 5 ? SecureRandom.hex(8) : counter
|
||||
"#{to_branch_name}-#{suffix}"
|
||||
end
|
||||
|
||||
Uniquify.new(start_counting_from).string(branch_name_generator) do |suggested_branch_name|
|
||||
project.repository.branch_exists?(suggested_branch_name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -677,24 +677,24 @@ class Repository
|
|||
@head_commit ||= commit(self.root_ref)
|
||||
end
|
||||
|
||||
def head_tree
|
||||
def head_tree(skip_flat_paths: true)
|
||||
if head_commit
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil)
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil, skip_flat_paths: skip_flat_paths)
|
||||
end
|
||||
end
|
||||
|
||||
def tree(sha = :head, path = nil, recursive: false, pagination_params: nil)
|
||||
def tree(sha = :head, path = nil, recursive: false, skip_flat_paths: true, pagination_params: nil)
|
||||
if sha == :head
|
||||
return unless head_commit
|
||||
|
||||
if path.nil?
|
||||
return head_tree
|
||||
return head_tree(skip_flat_paths: skip_flat_paths)
|
||||
else
|
||||
sha = head_commit.sha
|
||||
end
|
||||
end
|
||||
|
||||
Tree.new(self, sha, path, recursive: recursive, pagination_params: pagination_params)
|
||||
Tree.new(self, sha, path, recursive: recursive, skip_flat_paths: skip_flat_paths, pagination_params: pagination_params)
|
||||
end
|
||||
|
||||
def blob_at_branch(branch_name, path)
|
||||
|
|
|
@ -22,6 +22,8 @@ class Snippet < ApplicationRecord
|
|||
|
||||
MAX_FILE_COUNT = 10
|
||||
|
||||
DESCRIPTION_LENGTH_MAX = 1.megabyte
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
cache_markdown_field :content
|
||||
|
@ -57,19 +59,10 @@ class Snippet < ApplicationRecord
|
|||
validates :title, presence: true, length: { maximum: 255 }
|
||||
validates :file_name,
|
||||
length: { maximum: 255 }
|
||||
validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :description_changed?
|
||||
|
||||
validates :content, presence: true
|
||||
validates :content,
|
||||
length: {
|
||||
maximum: ->(_) { Gitlab::CurrentSettings.snippet_size_limit },
|
||||
message: -> (_, data) do
|
||||
current_value = ActiveSupport::NumberHelper.number_to_human_size(data[:value].size)
|
||||
max_size = ActiveSupport::NumberHelper.number_to_human_size(Gitlab::CurrentSettings.snippet_size_limit)
|
||||
|
||||
_("is too long (%{current_value}). The maximum size is %{max_size}.") % { current_value: current_value, max_size: max_size }
|
||||
end
|
||||
},
|
||||
if: :content_changed?
|
||||
validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }, if: :content_changed?
|
||||
|
||||
after_create :create_statistics
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class Tree
|
|||
|
||||
attr_accessor :repository, :sha, :path, :entries, :cursor
|
||||
|
||||
def initialize(repository, sha, path = '/', recursive: false, pagination_params: nil)
|
||||
def initialize(repository, sha, path = '/', recursive: false, skip_flat_paths: true, pagination_params: nil)
|
||||
path = '/' if path.blank?
|
||||
|
||||
@repository = repository
|
||||
|
@ -14,7 +14,7 @@ class Tree
|
|||
@path = path
|
||||
|
||||
git_repo = @repository.raw_repository
|
||||
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, pagination_params)
|
||||
@entries, @cursor = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
|
||||
def readme_path
|
||||
|
|
|
@ -5,12 +5,20 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
|
|||
|
||||
presents ::Commit, as: :commit
|
||||
|
||||
def status_for(ref)
|
||||
def detailed_status_for(ref)
|
||||
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
|
||||
return unless can?(current_user, :read_commit_status, commit.project)
|
||||
|
||||
commit.latest_pipeline(ref)&.detailed_status(current_user)
|
||||
end
|
||||
|
||||
def status_for(ref = nil)
|
||||
return unless can?(current_user, :read_pipeline, commit.latest_pipeline(ref))
|
||||
return unless can?(current_user, :read_commit_status, commit.project)
|
||||
|
||||
commit.status(ref)
|
||||
end
|
||||
|
||||
def any_pipelines?
|
||||
return false unless can?(current_user, :read_pipeline, commit.project)
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# BytesizeValidator
|
||||
#
|
||||
# Custom validator for verifying that bytesize of a field doesn't exceed the specified limit.
|
||||
# It is different from Rails length validator because it takes .bytesize into account instead of .size/.length
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class Snippet < ActiveRecord::Base
|
||||
# validates :content, bytesize: { maximum: -> { Gitlab::CurrentSettings.snippet_size_limit } }
|
||||
# end
|
||||
#
|
||||
# Configuration options:
|
||||
# * <tt>maximum</tt> - Proc that evaluates the bytesize limit that cannot be exceeded
|
||||
class BytesizeValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attr, value)
|
||||
size = value.to_s.bytesize
|
||||
max_size = options[:maximum].call
|
||||
|
||||
return if size <= max_size
|
||||
|
||||
error_message = format(_('is too long (%{size}). The maximum size is %{max_size}.'), {
|
||||
size: ActiveSupport::NumberHelper.number_to_human_size(size),
|
||||
max_size: ActiveSupport::NumberHelper.number_to_human_size(max_size)
|
||||
})
|
||||
|
||||
record.errors.add(attr, error_message)
|
||||
end
|
||||
end
|
|
@ -14,7 +14,7 @@
|
|||
- project = local_assigns.fetch(:project) { merge_request&.project }
|
||||
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
|
||||
- commit = commit.present(current_user: current_user)
|
||||
- commit_status = commit.status_for(ref)
|
||||
- commit_status = commit.detailed_status_for(ref)
|
||||
- collapsible = local_assigns.fetch(:collapsible, true)
|
||||
- link_data_attrs = local_assigns.fetch(:link_data_attrs, {})
|
||||
- link = commit_path(project, commit, merge_request: merge_request)
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: container_registry_legacy_authentication_for_deploy_tokens
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/2470
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365968
|
||||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if Gem.loaded_specs['rack'].version >= Gem::Version.new("3.0.0")
|
||||
raise <<~ERR
|
||||
This patch is unnecessary in Rack versions 3.0.0 or newer.
|
||||
Please remove this file and the associated spec.
|
||||
|
||||
See https://github.com/rack/rack/blob/main/CHANGELOG.md#security (issue #1733)
|
||||
ERR
|
||||
end
|
||||
|
||||
# Patches a cache poisoning attack vector in Rack by not allowing semicolons
|
||||
# to delimit query parameters.
|
||||
# See https://github.com/rack/rack/issues/1732.
|
||||
#
|
||||
# Solution is taken from the same issue.
|
||||
#
|
||||
# The actual patch is due for release in Rack 3.0.0.
|
||||
module Rack
|
||||
class Request
|
||||
Helpers.module_eval do
|
||||
# rubocop: disable Naming/MethodName
|
||||
def GET
|
||||
if get_header(RACK_REQUEST_QUERY_STRING) == query_string
|
||||
get_header(RACK_REQUEST_QUERY_HASH)
|
||||
else
|
||||
query_hash = parse_query(query_string, '&') # only allow ampersand here
|
||||
set_header(RACK_REQUEST_QUERY_STRING, query_string)
|
||||
set_header(RACK_REQUEST_QUERY_HASH, query_hash)
|
||||
end
|
||||
end
|
||||
# rubocop: enable Naming/MethodName
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
# This patch updates SawyerResource class to not allow Ruby methods to be overridden and accessed.
|
||||
# Any attempt to access a Ruby method will result in an exception.
|
||||
module SawyerClassPatch
|
||||
def attr_accessor(*attrs)
|
||||
attrs.each do |attribute|
|
||||
class_eval do
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
if method_defined?(attribute) || method_defined?("#{attribute}=") || method_defined?("#{attribute}?")
|
||||
define_method attribute do
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
|
||||
define_method "#{attribute}=" do |value|
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
|
||||
define_method "#{attribute}?" do
|
||||
raise Sawyer::Error,
|
||||
"Sawyer method \"#{attribute}\" overlaps Ruby method. Convert to a hash to access the attribute."
|
||||
end
|
||||
else
|
||||
define_method attribute do
|
||||
@attrs[attribute.to_sym]
|
||||
end
|
||||
|
||||
define_method "#{attribute}=" do |value|
|
||||
@attrs[attribute.to_sym] = value
|
||||
end
|
||||
|
||||
define_method "#{attribute}?" do
|
||||
!!@attrs[attribute.to_sym]
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Sawyer::Resource.singleton_class.prepend(SawyerClassPatch)
|
|
@ -47,7 +47,7 @@ In addition, it:
|
|||
Geo provides:
|
||||
|
||||
- Read-only **secondary** sites: Maintain one **primary** GitLab site while still enabling read-only **secondary** sites for each of your distributed teams.
|
||||
- Authentication system hooks: **Secondary** sites receives all authentication data (like user accounts and logins) from the **primary** instance.
|
||||
- Authentication system hooks: **Secondary** sites receive all authentication data (like user accounts and logins) from the **primary** instance.
|
||||
- An intuitive UI: **Secondary** sites use the same web interface your team has grown accustomed to. In addition, there are visual notifications that block write operations and make it clear that a user is on a **secondary** sites.
|
||||
|
||||
### Gitaly Cluster
|
||||
|
|
|
@ -51,7 +51,7 @@ Memory, CPU, and storage resource amounts vary depending on the amount of data y
|
|||
each node should have:
|
||||
|
||||
- [Memory](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_memory): 8 GiB (minimum).
|
||||
- [CPU](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_cpus): Modern processor with multiple cores. GitLab.com has minimal CPU requirements for Elasticsearch. Multiple cores provide extra concurrency, which is more beneficial than faster CPUs.
|
||||
- [CPU](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_cpus): Modern processor with multiple cores. GitLab has minimal CPU requirements for Elasticsearch. Multiple cores provide extra concurrency, which is more beneficial than faster CPUs.
|
||||
- [Storage](https://www.elastic.co/guide/en/elasticsearch/guide/current/hardware.html#_disks): Use SSD storage. The total storage size of all Elasticsearch nodes is about 50% of the total size of your Git repositories. It includes one primary and one replica. The [`estimate_cluster_size`](#gitlab-advanced-search-rake-tasks) Rake task ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221177) in GitLab 13.10) uses total repository size to estimate the Advanced Search storage requirements.
|
||||
|
||||
## Install Elasticsearch
|
||||
|
|
|
@ -267,3 +267,8 @@ To resolve this issue, you can update the password expiration by either:
|
|||
```
|
||||
|
||||
The bug was reported [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/332455).
|
||||
|
||||
## Error on Git fetch: "HTTP Basic: Access Denied"
|
||||
|
||||
If you receive an `HTTP Basic: Access denied` error when using Git over HTTP(S),
|
||||
refer to the [two-factor authentication troubleshooting guide](../../user/profile/account/two_factor_authentication.md#troubleshooting).
|
||||
|
|
|
@ -299,6 +299,10 @@ hub_docker_quota_check:
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
## Authentication error: "HTTP Basic: Access Denied"
|
||||
|
||||
If you receive an `HTTP Basic: Access denied` error when authenticating against the Dependency Proxy, refer to the [two-factor authentication troubleshooting guide](../../profile/account/two_factor_authentication.md#troubleshooting).
|
||||
|
||||
### Dependency Proxy Connection Failure
|
||||
|
||||
If a service alias is not set the `docker:20.10.16` image is unable to find the
|
||||
|
|
|
@ -345,6 +345,11 @@ when a PyPI package is not found in the Package Registry, the request is forward
|
|||
|
||||
Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
|
||||
|
||||
WARNING:
|
||||
When you use the `--index-url` option, do not specify the port if it is a default
|
||||
port, such as `80` for a URL starting with `http` or `443` for a URL starting
|
||||
with `https`.
|
||||
|
||||
### Install from the project level
|
||||
|
||||
To install the latest version of a package, use the following command:
|
||||
|
|
|
@ -427,6 +427,39 @@ a GitLab global administrator disable 2FA for your account:
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "HTTP Basic: Access denied. The provided password or token ..."
|
||||
|
||||
When making a request, you can receive the following error:
|
||||
|
||||
```plaintext
|
||||
HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal
|
||||
access token instead of a password.
|
||||
```
|
||||
|
||||
This error occurs in the following scenarios:
|
||||
|
||||
- You have 2FA enabled and have attempted to authenticate with a username and
|
||||
password. For 2FA-enabled users, a [personal access token](../personal_access_tokens.md) (PAT)
|
||||
must be used instead of a password. To authenticate:
|
||||
- Git requests over HTTP(S), a PAT with `read_repository` or `write_repository` scope is required.
|
||||
- [GitLab Container Registry](../../packages/container_registry/index.md#authenticate-with-the-container-registry) requests, a PAT
|
||||
with `read_registry` or `write_registry` scope is required.
|
||||
- [Dependency Proxy](../../packages/dependency_proxy/index.md#authenticate-with-the-dependency-proxy) requests, a PAT with
|
||||
`read_registry` and `write_registry` scopes is required.
|
||||
- You do not have 2FA enabled and have sent an incorrect username or password
|
||||
with your request.
|
||||
- You do not have 2FA enabled but an administrator has enabled the
|
||||
[enforce 2FA for all users](../../../security/two_factor_authentication.md#enforce-2fa-for-all-users) setting.
|
||||
- You do not have 2FA enabled, but an administrator has disabled the
|
||||
[password authentication enabled for Git over HTTP(S)](../../admin_area/settings/sign_in_restrictions.md#password-authentication-enabled)
|
||||
setting. If LDAP is:
|
||||
- Configured, an [LDAP password](../../../administration/auth/ldap/index.md)
|
||||
or a [personal access token](../personal_access_tokens.md)
|
||||
must be used to authenticate Git requests over HTTP(S).
|
||||
- Not configured, you must use a [personal access token](../personal_access_tokens.md).
|
||||
|
||||
### Error: "invalid pin code"
|
||||
|
||||
If you receive an `invalid pin code` error, this can indicate that there is a time sync issue between the authentication
|
||||
application and the GitLab instance itself. To avoid the time sync issue, enable time synchronization in the device that
|
||||
generates the codes. For example:
|
||||
|
|
|
@ -100,7 +100,7 @@ To delete a task:
|
|||
1. In the task window, in the options menu (**{ellipsis_v}**), select **Delete task**.
|
||||
1. Select **OK**.
|
||||
|
||||
## Set task weight **PREMIUM**
|
||||
## Set task weight **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.3.
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ module API
|
|||
Gitlab::UsageDataCounters::EditorUniqueCounter.track_web_ide_edit_action(author: current_user, project: user_project)
|
||||
end
|
||||
|
||||
present commit_detail, with: Entities::CommitDetail, stats: params[:stats]
|
||||
present commit_detail, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
|
||||
else
|
||||
render_api_error!(result[:message], 400)
|
||||
end
|
||||
|
@ -163,7 +163,7 @@ module API
|
|||
|
||||
not_found! 'Commit' unless commit
|
||||
|
||||
present commit, with: Entities::CommitDetail, stats: params[:stats], current_user: current_user
|
||||
present commit, with: Entities::CommitDetail, include_stats: params[:stats], current_user: current_user
|
||||
end
|
||||
|
||||
desc 'Get the diff for a specific commit of a project' do
|
||||
|
|
|
@ -12,7 +12,9 @@ module API
|
|||
expose :trailers
|
||||
|
||||
expose :web_url do |commit, _options|
|
||||
Gitlab::UrlBuilder.build(commit)
|
||||
c = commit
|
||||
c = c.__subject__ if c.is_a?(Gitlab::View::Presenter::Base)
|
||||
Gitlab::UrlBuilder.build(c)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
module API
|
||||
module Entities
|
||||
class CommitDetail < Commit
|
||||
expose :stats, using: Entities::CommitStats, if: :stats
|
||||
expose :status
|
||||
include ::API::Helpers::Presentable
|
||||
|
||||
expose :stats, using: Entities::CommitStats, if: :include_stats
|
||||
expose :status_for, as: :status
|
||||
expose :project_id
|
||||
|
||||
expose :last_pipeline do |commit, options|
|
||||
|
|
|
@ -14,28 +14,12 @@ module API
|
|||
include Constants
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def unauthorized_user_project
|
||||
@unauthorized_user_project ||= find_project(params[:id])
|
||||
end
|
||||
|
||||
def unauthorized_user_project!
|
||||
unauthorized_user_project || not_found!
|
||||
end
|
||||
|
||||
def unauthorized_user_group
|
||||
@unauthorized_user_group ||= find_group(params[:id])
|
||||
end
|
||||
|
||||
def unauthorized_user_group!
|
||||
unauthorized_user_group || not_found!
|
||||
end
|
||||
|
||||
def authorized_user_project
|
||||
@authorized_user_project ||= authorized_project_find!
|
||||
end
|
||||
|
||||
def authorized_project_find!
|
||||
project = unauthorized_user_project
|
||||
project = find_project(params[:id])
|
||||
|
||||
unless project && can?(current_user, :read_project, project)
|
||||
return unauthorized_or! { not_found! }
|
||||
|
|
|
@ -84,6 +84,16 @@ module API
|
|||
|
||||
body content
|
||||
end
|
||||
|
||||
def ensure_group!
|
||||
find_group(params[:id]) || not_found!
|
||||
find_authorized_group!
|
||||
end
|
||||
|
||||
def ensure_project!
|
||||
find_project(params[:id]) || not_found!
|
||||
authorized_user_project
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
|
@ -91,7 +101,7 @@ module API
|
|||
end
|
||||
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
after_validation do
|
||||
unauthorized_user_group!
|
||||
ensure_group!
|
||||
end
|
||||
|
||||
namespace ':id/-/packages/pypi' do
|
||||
|
@ -101,7 +111,8 @@ module API
|
|||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'files/:sha256/*file_identifier' do
|
||||
group = unauthorized_user_group!
|
||||
group = find_authorized_group!
|
||||
authorize_read_package!(group)
|
||||
|
||||
filename = "#{params[:file_identifier]}.#{params[:format]}"
|
||||
package = Packages::Pypi::PackageFinder.new(current_user, group, { filename: filename, sha256: params[:sha256] }).execute
|
||||
|
@ -146,7 +157,7 @@ module API
|
|||
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
before do
|
||||
unauthorized_user_project!
|
||||
ensure_project!
|
||||
end
|
||||
|
||||
namespace ':id/packages/pypi' do
|
||||
|
@ -160,7 +171,8 @@ module API
|
|||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'files/:sha256/*file_identifier' do
|
||||
project = unauthorized_user_project!
|
||||
project = authorized_user_project
|
||||
authorize_read_package!(project)
|
||||
|
||||
filename = "#{params[:file_identifier]}.#{params[:format]}"
|
||||
package = Packages::Pypi::PackageFinder.new(current_user, project, { filename: filename, sha256: params[:sha256] }).execute
|
||||
|
|
|
@ -189,7 +189,7 @@ module API
|
|||
compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight])
|
||||
|
||||
if compare
|
||||
present compare, with: Entities::Compare
|
||||
present compare, with: Entities::Compare, current_user: current_user
|
||||
else
|
||||
not_found!("Ref")
|
||||
end
|
||||
|
|
|
@ -123,7 +123,7 @@ module API
|
|||
get do
|
||||
verify_search_scope!(resource: nil)
|
||||
|
||||
present search, with: entity
|
||||
present search, with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -145,7 +145,7 @@ module API
|
|||
get ':id/(-/)search' do
|
||||
verify_search_scope!(resource: user_group)
|
||||
|
||||
present search(group_id: user_group.id), with: entity
|
||||
present search(group_id: user_group.id), with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -166,7 +166,7 @@ module API
|
|||
use :pagination
|
||||
end
|
||||
get ':id/(-/)search' do
|
||||
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity
|
||||
present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity, current_user: current_user
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ module API
|
|||
|
||||
if result[:status] == :success
|
||||
commit_detail = user_project.repository.commit(result[:result])
|
||||
present commit_detail, with: Entities::CommitDetail
|
||||
present commit_detail, with: Entities::CommitDetail, current_user: current_user
|
||||
else
|
||||
render_api_error!(result[:message], result[:http_status] || 400)
|
||||
end
|
||||
|
|
|
@ -17,21 +17,10 @@ module Banzai
|
|||
include ActionView::Helpers::TagHelper
|
||||
include AvatarsHelper
|
||||
|
||||
TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
|
||||
AUTHOR_REGEXP = /(?<author_name>.+)/.freeze
|
||||
# Devise.email_regexp wouldn't work here since its designed to match
|
||||
# against strings that only contains email addresses; the \A and \z
|
||||
# around the expression will only match if the string being matched
|
||||
# contains just the email nothing else.
|
||||
MAIL_REGEXP = /<(?<author_email>[^@\s]+@[^@\s]+)>/.freeze
|
||||
FILTER_REGEXP = /(?<trailer>^\s*#{TRAILER_REGEXP}\s*#{AUTHOR_REGEXP}\s+#{MAIL_REGEXP}$)/mi.freeze
|
||||
|
||||
def call
|
||||
doc.xpath('descendant-or-self::text()').each do |node|
|
||||
content = node.to_html
|
||||
|
||||
next unless content.match(FILTER_REGEXP)
|
||||
|
||||
html = trailer_filter(content)
|
||||
|
||||
next if html == content
|
||||
|
@ -52,11 +41,24 @@ module Banzai
|
|||
# Returns a String with all trailer lines replaced with links to GitLab
|
||||
# users and mailto links to non GitLab users. All links have `data-trailer`
|
||||
# and `data-user` attributes attached.
|
||||
#
|
||||
# The code intentionally avoids using Regex for security and performance
|
||||
# reasons: https://gitlab.com/gitlab-org/gitlab/-/issues/363734
|
||||
def trailer_filter(text)
|
||||
text.gsub(FILTER_REGEXP) do |author_match|
|
||||
label = $~[:label]
|
||||
"#{label} #{parse_user($~[:author_name], $~[:author_email], label)}"
|
||||
end
|
||||
text.lines.map! do |line|
|
||||
trailer, rest = line.split(':', 2)
|
||||
|
||||
next line unless trailer.downcase.end_with?('-by')
|
||||
|
||||
chunks = rest.split
|
||||
author_email = chunks.pop.delete_prefix('<').delete_suffix('>')
|
||||
next line unless Devise.email_regexp.match(author_email)
|
||||
|
||||
author_name = chunks.join(' ').strip
|
||||
trailer = "#{trailer.strip}:"
|
||||
|
||||
"#{trailer} #{link_to_user_or_email(author_name, author_email, trailer)}\n"
|
||||
end.join
|
||||
end
|
||||
|
||||
# Find a GitLab user using the supplied email and generate
|
||||
|
@ -67,7 +69,7 @@ module Banzai
|
|||
# trailer - String trailer used in the commit message
|
||||
#
|
||||
# Returns a String with a link to the user.
|
||||
def parse_user(name, email, trailer)
|
||||
def link_to_user_or_email(name, email, trailer)
|
||||
link_to_user User.find_by_any_email(email),
|
||||
name: name,
|
||||
email: email,
|
||||
|
|
|
@ -34,17 +34,20 @@ module Banzai
|
|||
img.remove_attribute('data-diagram-src')
|
||||
end
|
||||
|
||||
link.children = if link_replaces_image
|
||||
img['alt'] || img['data-src'] || img['src']
|
||||
else
|
||||
img.clone
|
||||
end
|
||||
link.children = link_replaces_image ? link_children(img) : img.clone
|
||||
|
||||
img.replace(link)
|
||||
end
|
||||
|
||||
doc
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def link_children(img)
|
||||
[img['alt'], img['data-src'], img['src']]
|
||||
.map { |f| Sanitize.fragment(f).presence }.compact.first || ''
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module Filter
|
||||
class PathologicalMarkdownFilter < HTML::Pipeline::TextFilter
|
||||
# It's not necessary for this to be precise - we just need to detect
|
||||
# when there are a non-trivial number of unclosed image links.
|
||||
# So we don't really care about code blocks, etc.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/370428
|
||||
REGEX = /!\[(?:[^\]])+?!\[/.freeze
|
||||
DETECTION_MAX = 10
|
||||
|
||||
def call
|
||||
count = 0
|
||||
|
||||
@text.scan(REGEX) do |_match|
|
||||
count += 1
|
||||
break if count > DETECTION_MAX
|
||||
end
|
||||
|
||||
return @text if count <= DETECTION_MAX
|
||||
|
||||
"_Unable to render markdown - too many unclosed markdown image links detected._"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ module Banzai
|
|||
class PlainMarkdownPipeline < BasePipeline
|
||||
def self.filters
|
||||
FilterArray[
|
||||
Filter::PathologicalMarkdownFilter,
|
||||
Filter::MarkdownPreEscapeFilter,
|
||||
Filter::MarkdownFilter,
|
||||
Filter::MarkdownPostEscapeFilter
|
||||
|
|
|
@ -16,9 +16,10 @@ module Gitlab
|
|||
TREE_SORT_ORDER = { tree: 0, blob: 1, commit: 2 }.freeze
|
||||
|
||||
override :tree_entries
|
||||
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
|
||||
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
|
||||
if use_rugged?(repository, :rugged_tree_entries)
|
||||
entries = execute_rugged_call(:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive)
|
||||
entries = execute_rugged_call(
|
||||
:tree_entries_with_flat_path_from_rugged, repository, sha, path, recursive, skip_flat_paths)
|
||||
|
||||
if pagination_params
|
||||
paginated_response(entries, pagination_params[:limit], pagination_params[:page_token].to_s)
|
||||
|
@ -60,11 +61,11 @@ module Gitlab
|
|||
[result, cursor]
|
||||
end
|
||||
|
||||
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive)
|
||||
def tree_entries_with_flat_path_from_rugged(repository, sha, path, recursive, skip_flat_paths)
|
||||
tree_entries_from_rugged(repository, sha, path, recursive).tap do |entries|
|
||||
# This was an optimization to reduce N+1 queries for Gitaly
|
||||
# (https://gitlab.com/gitlab-org/gitaly/issues/530).
|
||||
rugged_populate_flat_path(repository, sha, path, entries)
|
||||
rugged_populate_flat_path(repository, sha, path, entries) unless skip_flat_paths
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,15 +15,16 @@ module Gitlab
|
|||
# Uses rugged for raw objects
|
||||
#
|
||||
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
|
||||
def where(repository, sha, path = nil, recursive = false, pagination_params = nil)
|
||||
def where(repository, sha, path = nil, recursive = false, skip_flat_paths = true, pagination_params = nil)
|
||||
path = nil if path == '' || path == '/'
|
||||
|
||||
tree_entries(repository, sha, path, recursive, pagination_params)
|
||||
tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
|
||||
def tree_entries(repository, sha, path, recursive, pagination_params = nil)
|
||||
def tree_entries(repository, sha, path, recursive, skip_flat_paths, pagination_params = nil)
|
||||
wrapped_gitaly_errors do
|
||||
repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive, pagination_params)
|
||||
repository.gitaly_commit_client.tree_entries(
|
||||
repository, sha, path, recursive, skip_flat_paths, pagination_params)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ module Gitlab
|
|||
class CommitService
|
||||
include Gitlab::EncodingHelper
|
||||
|
||||
TREE_ENTRIES_DEFAULT_LIMIT = 100_000
|
||||
|
||||
def initialize(repository)
|
||||
@gitaly_repo = repository.gitaly_repository
|
||||
@repository = repository
|
||||
|
@ -111,12 +113,16 @@ module Gitlab
|
|||
nil
|
||||
end
|
||||
|
||||
def tree_entries(repository, revision, path, recursive, pagination_params)
|
||||
def tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params)
|
||||
pagination_params ||= {}
|
||||
pagination_params[:limit] ||= TREE_ENTRIES_DEFAULT_LIMIT
|
||||
|
||||
request = Gitaly::GetTreeEntriesRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
revision: encode_binary(revision),
|
||||
path: path.present? ? encode_binary(path) : '.',
|
||||
recursive: recursive,
|
||||
skip_flat_paths: skip_flat_paths,
|
||||
pagination_params: pagination_params
|
||||
)
|
||||
request.sort = Gitaly::GetTreeEntriesRequest::SortBy::TREES_FIRST if pagination_params
|
||||
|
|
|
@ -11,7 +11,7 @@ module Gitlab
|
|||
# this if the change to the renderer output is a new feature or a
|
||||
# minor bug fix.
|
||||
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/330313
|
||||
CACHE_COMMONMARK_VERSION = 31
|
||||
CACHE_COMMONMARK_VERSION = 32
|
||||
CACHE_COMMONMARK_VERSION_START = 10
|
||||
|
||||
BaseError = Class.new(StandardError)
|
||||
|
|
|
@ -68,6 +68,10 @@ module Gitlab
|
|||
with { |redis| redis.ttl(cache_key(key)) }
|
||||
end
|
||||
|
||||
def count(key)
|
||||
with { |redis| redis.scard(cache_key(key)) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def with(&blk)
|
||||
|
|
|
@ -5,6 +5,10 @@ module Gitlab
|
|||
class Client
|
||||
Error = Class.new(StandardError)
|
||||
ConfigError = Class.new(Error)
|
||||
RequestError = Class.new(Error)
|
||||
|
||||
CACHE_MAX_SET_SIZE = 5_000
|
||||
CACHE_TTL = 1.month.freeze
|
||||
|
||||
attr_reader :integration
|
||||
|
||||
|
@ -33,11 +37,21 @@ module Gitlab
|
|||
end
|
||||
|
||||
def fetch_issues(params = {})
|
||||
get("products/#{zentao_product_xid}/issues", params)
|
||||
get("products/#{zentao_product_xid}/issues", params).tap do |response|
|
||||
mark_issues_as_seen_in_product(response['issues'])
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_issue(issue_id)
|
||||
raise Gitlab::Zentao::Client::Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
|
||||
raise Error, 'invalid issue id' unless issue_id_pattern.match(issue_id)
|
||||
|
||||
# Only return issues that are associated with the product configured in
|
||||
# the integration. Due to a lack of available data in the ZenTao APIs, we
|
||||
# can only determine if an issue belongs to a product if the issue was
|
||||
# previously returned in the `#fetch_issues` call.
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/360372#note_1016963713
|
||||
raise RequestError unless issue_seen_in_product?(issue_id)
|
||||
|
||||
get("issues/#{issue_id}")
|
||||
end
|
||||
|
@ -52,17 +66,15 @@ module Gitlab
|
|||
options = { headers: headers, query: params }
|
||||
response = Gitlab::HTTP.get(url(path), options)
|
||||
|
||||
raise Gitlab::Zentao::Client::Error, 'request error' unless response.success?
|
||||
raise RequestError unless response.success?
|
||||
|
||||
Gitlab::Json.parse(response.body)
|
||||
rescue JSON::ParserError
|
||||
raise Gitlab::Zentao::Client::Error, 'invalid response format'
|
||||
raise Error, 'invalid response format'
|
||||
end
|
||||
|
||||
def url(path)
|
||||
host = integration.api_url.presence || integration.url
|
||||
|
||||
URI.parse(Gitlab::Utils.append_path(host, "api.php/v1/#{path}"))
|
||||
URI.parse(Gitlab::Utils.append_path(integration.client_url, "api.php/v1/#{path}"))
|
||||
end
|
||||
|
||||
def headers
|
||||
|
@ -75,6 +87,30 @@ module Gitlab
|
|||
def zentao_product_xid
|
||||
integration.zentao_product_xid
|
||||
end
|
||||
|
||||
def issue_ids_cache_key
|
||||
@issue_ids_cache_key ||= [
|
||||
:zentao_product_issues,
|
||||
OpenSSL::Digest::SHA256.hexdigest(integration.client_url),
|
||||
zentao_product_xid
|
||||
].join(':')
|
||||
end
|
||||
|
||||
def issue_ids_cache
|
||||
@issue_ids_cache ||= ::Gitlab::SetCache.new(expires_in: CACHE_TTL)
|
||||
end
|
||||
|
||||
def mark_issues_as_seen_in_product(issues)
|
||||
return unless issues && issue_ids_cache.count(issue_ids_cache_key) < CACHE_MAX_SET_SIZE
|
||||
|
||||
ids = issues.map { _1['id'] }
|
||||
|
||||
issue_ids_cache.write(issue_ids_cache_key, ids)
|
||||
end
|
||||
|
||||
def issue_seen_in_product?(id)
|
||||
issue_ids_cache.include?(issue_ids_cache_key, id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19169,7 +19169,7 @@ msgstr ""
|
|||
msgid "HTTP Archive (HAR)"
|
||||
msgstr ""
|
||||
|
||||
msgid "HTTP Basic: Access denied\\nYou must use a personal access token with 'api' scope for Git over HTTP.\\nYou can generate one at %{profile_personal_access_tokens_url}"
|
||||
msgid "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See %{help_page_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Harbor Registry"
|
||||
|
@ -20872,6 +20872,9 @@ msgstr ""
|
|||
msgid "Incident|Error deleting incident timeline event: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Error updating incident timeline event: %{error}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Metrics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20890,6 +20893,9 @@ msgstr ""
|
|||
msgid "Incident|Something went wrong while fetching incident timeline events."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Something went wrong while updating the incident timeline event."
|
||||
msgstr ""
|
||||
|
||||
msgid "Incident|Summary"
|
||||
msgstr ""
|
||||
|
||||
|
@ -46721,6 +46727,9 @@ msgstr ""
|
|||
msgid "is too long (%{current_value}). The maximum size is %{max_size}."
|
||||
msgstr ""
|
||||
|
||||
msgid "is too long (%{size}). The maximum size is %{max_size}."
|
||||
msgstr ""
|
||||
|
||||
msgid "is too long (maximum is %{count} characters)"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
"@apollo/client": "^3.5.10",
|
||||
"@babel/core": "^7.18.5",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@codesandbox/sandpack-client": "^1.2.2",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "3.2.0",
|
||||
|
@ -164,7 +165,6 @@
|
|||
"remark-rehype": "^10.1.0",
|
||||
"scrollparent": "^2.0.1",
|
||||
"select2": "3.5.2-browserify",
|
||||
"smooshpack": "^0.0.62",
|
||||
"sortablejs": "^1.10.2",
|
||||
"string-hash": "1.1.3",
|
||||
"style-loader": "^2.0.0",
|
||||
|
|
|
@ -16,4 +16,4 @@ install:
|
|||
script:
|
||||
- "pip install <%= package.name %> --no-deps --index-url <%= uri.scheme %>://<%= personal_access_token %>:<%= personal_access_token %>@<%= gitlab_host_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host <%= gitlab_host_with_port %>"
|
||||
tags:
|
||||
- runner-for-<%= project.name %>
|
||||
- runner-for-<%= project.name %>
|
||||
|
|
|
@ -30,9 +30,16 @@ module QA
|
|||
end
|
||||
|
||||
let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
|
||||
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
|
||||
let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
|
||||
let(:personal_access_token) { use_ci_variable(name: 'PERSONAL_ACCESS_TOKEN', value: Runtime::Env.personal_access_token, project: project) }
|
||||
let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
|
||||
let(:gitlab_host_with_port) do
|
||||
# Don't specify port if it is a standard one
|
||||
if uri.port == 80 || uri.port == 443
|
||||
uri.host
|
||||
else
|
||||
"#{uri.host}:#{uri.port}"
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
|
|
|
@ -29,27 +29,28 @@ RSpec.describe 'Render Static HTML', :api, type: :request do # rubocop:disable R
|
|||
include Glfm::Constants
|
||||
include Glfm::Shared
|
||||
|
||||
let(:user) { create(:user, :admin, username: 'glfm_user') }
|
||||
# TODO: Remove duplication of fixtures & logic with spec/support/shared_contexts/markdown_snapshot_shared_examples.rb
|
||||
|
||||
before do
|
||||
stub_licensed_features(group_wikis: true)
|
||||
|
||||
group = create(:group, name: 'glfm_group')
|
||||
group.add_owner(user)
|
||||
|
||||
project = create(:project, :repository, creator: user, group: group, name: 'glfm_project')
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, name: 'glfm_group').tap { |group| group.add_owner(user) } }
|
||||
|
||||
let_it_be(:project) do
|
||||
# NOTE: We hardcode the IDs on all fixtures to prevent variability in the
|
||||
# rendered HTML/Prosemirror JSON, and to minimize the need for normalization:
|
||||
# https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#normalization
|
||||
create(:project_snippet, id: 88888, project: project) # project snippet
|
||||
create(:snippet, id: 99999) # personal snippet
|
||||
create(:project, :repository, creator: user, group: group, name: 'glfm_project', id: 77777)
|
||||
end
|
||||
|
||||
let_it_be(:project_snippet) { create(:project_snippet, id: 88888, project: project) }
|
||||
let_it_be(:personal_snippet) { create(:snippet, id: 99999) }
|
||||
|
||||
before do
|
||||
stub_licensed_features(group_wikis: true)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'can create a project dependency graph using factories' do
|
||||
markdown_hash = YAML.load_file(ENV.fetch('INPUT_MARKDOWN_YML_PATH'))
|
||||
markdown_hash = YAML.safe_load(File.open(ENV.fetch('INPUT_MARKDOWN_YML_PATH')), symbolize_names: true)
|
||||
|
||||
# NOTE: We cannot parallelize this loop like the Javascript WYSIWYG example generation does,
|
||||
# because the rspec `post` API cannot be parallized (it is not thread-safe, it can't find
|
||||
|
@ -58,12 +59,14 @@ RSpec.describe 'Render Static HTML', :api, type: :request do # rubocop:disable R
|
|||
api_url = api "/markdown"
|
||||
|
||||
post api_url, params: { text: markdown, gfm: true }
|
||||
# noinspection RubyResolve
|
||||
expect(response).to be_successful
|
||||
|
||||
returned_html_value =
|
||||
begin
|
||||
parsed_response = Gitlab::Json.parse(response.body)
|
||||
# The response may contain the HTML in either the `body` or `html` keys
|
||||
parsed_response['body'] || parsed_response['html']
|
||||
parsed_response = Gitlab::Json.parse(response.body, symbolize_names: true)
|
||||
# Some responses have the HTML in the `html` key, others in the `body` key.
|
||||
parsed_response[:body] || parsed_response[:html]
|
||||
rescue JSON::ParserError
|
||||
# if we got a parsing error, just return the raw response body for debugging purposes.
|
||||
response.body
|
||||
|
@ -79,7 +82,7 @@ RSpec.describe 'Render Static HTML', :api, type: :request do # rubocop:disable R
|
|||
|
||||
def write_output_file(static_html_hash)
|
||||
tmpfile = File.open(ENV.fetch('OUTPUT_STATIC_HTML_TEMPFILE_PATH'), 'w')
|
||||
YAML.dump(static_html_hash, tmpfile)
|
||||
tmpfile.close
|
||||
yaml_string = dump_yaml_with_formatting(static_html_hash)
|
||||
write_file(tmpfile, yaml_string)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fileutils'
|
||||
require 'open3'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
|
||||
module Glfm
|
||||
module Shared
|
||||
|
@ -39,5 +40,38 @@ module Glfm
|
|||
warn(stdout_and_stderr_str)
|
||||
raise
|
||||
end
|
||||
|
||||
# Construct an AST so we can control YAML formatting for
|
||||
# YAML block scalar literals and key quoting.
|
||||
#
|
||||
# Note that when Psych dumps the markdown to YAML, it will
|
||||
# automatically use the default "clip" behavior of the Block Chomping Indicator (`|`)
|
||||
# https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator,
|
||||
# when the markdown strings contain a trailing newline. The type of
|
||||
# Block Chomping Indicator is automatically determined, you cannot specify it
|
||||
# manually.
|
||||
def dump_yaml_with_formatting(hash, literal_scalars: false)
|
||||
stringified_keys_hash = hash.deep_stringify_keys
|
||||
visitor = Psych::Visitors::YAMLTree.create
|
||||
visitor << stringified_keys_hash
|
||||
ast = visitor.tree
|
||||
|
||||
# Force all scalars to have literal formatting (using Block Chomping Indicator instead of quotes)
|
||||
if literal_scalars
|
||||
ast.grep(Psych::Nodes::Scalar).each do |node|
|
||||
node.style = Psych::Nodes::Scalar::LITERAL
|
||||
end
|
||||
end
|
||||
|
||||
# Do not quote the keys
|
||||
ast.grep(Psych::Nodes::Mapping).each do |node|
|
||||
node.children.each_slice(2) do |k, _|
|
||||
k.quoted = false
|
||||
k.style = Psych::Nodes::Scalar::PLAIN
|
||||
end
|
||||
end
|
||||
|
||||
ast.to_yaml
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -125,7 +125,7 @@ module Glfm
|
|||
|
||||
def write_snapshot_example_files(all_examples, skip_static_and_wysiwyg:)
|
||||
output("Reading #{GLFM_EXAMPLE_STATUS_YML_PATH}...")
|
||||
glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH))
|
||||
glfm_examples_statuses = YAML.safe_load(File.open(GLFM_EXAMPLE_STATUS_YML_PATH), symbolize_names: true)
|
||||
validate_glfm_example_status_yml(glfm_examples_statuses)
|
||||
|
||||
write_examples_index_yml(all_examples)
|
||||
|
@ -153,8 +153,8 @@ module Glfm
|
|||
def validate_glfm_example_status_yml(glfm_examples_statuses)
|
||||
glfm_examples_statuses.each do |example_name, statuses|
|
||||
next unless statuses &&
|
||||
statuses['skip_update_example_snapshots'] &&
|
||||
statuses.any? { |key, value| key.include?('skip_update_example_snapshot_') && !!value }
|
||||
statuses[:skip_update_example_snapshots] &&
|
||||
statuses.any? { |key, value| key.to_s.include?('skip_update_example_snapshot_') && !!value }
|
||||
|
||||
raise "Error: '#{example_name}' must not have any 'skip_update_example_snapshot_*' values specified " \
|
||||
"if 'skip_update_example_snapshots' is truthy"
|
||||
|
@ -165,7 +165,8 @@ module Glfm
|
|||
generate_and_write_for_all_examples(
|
||||
all_examples, ES_EXAMPLES_INDEX_YML_PATH, literal_scalars: false
|
||||
) do |example, hash|
|
||||
hash[example.fetch(:name)] = {
|
||||
name = example.fetch(:name).to_sym
|
||||
hash[name] = {
|
||||
'spec_txt_example_position' => example.fetch(:example),
|
||||
'source_specification' =>
|
||||
if example[:extensions].empty?
|
||||
|
@ -181,7 +182,8 @@ module Glfm
|
|||
|
||||
def write_markdown_yml(all_examples)
|
||||
generate_and_write_for_all_examples(all_examples, ES_MARKDOWN_YML_PATH) do |example, hash|
|
||||
hash[example.fetch(:name)] = example.fetch(:markdown)
|
||||
name = example.fetch(:name).to_sym
|
||||
hash[name] = example.fetch(:markdown)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -225,7 +227,7 @@ module Glfm
|
|||
run_external_cmd(cmd)
|
||||
|
||||
output("Reading generated static HTML from tempfile #{static_html_tempfile_path}...")
|
||||
YAML.load_file(static_html_tempfile_path)
|
||||
YAML.safe_load(File.open(static_html_tempfile_path), symbolize_names: true)
|
||||
end
|
||||
|
||||
def generate_wysiwyg_html_and_json
|
||||
|
@ -241,26 +243,26 @@ module Glfm
|
|||
|
||||
output("Reading generated WYSIWYG HTML and prosemirror JSON from tempfile " \
|
||||
"#{wysiwyg_html_and_json_tempfile_path}...")
|
||||
YAML.load_file(wysiwyg_html_and_json_tempfile_path)
|
||||
YAML.safe_load(File.open(wysiwyg_html_and_json_tempfile_path), symbolize_names: true)
|
||||
end
|
||||
|
||||
def write_html_yml(all_examples, static_html_hash, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
generate_and_write_for_all_examples(
|
||||
all_examples, ES_HTML_YML_PATH, glfm_examples_statuses
|
||||
all_examples, ES_HTML_YML_PATH, glfm_examples_statuses: glfm_examples_statuses
|
||||
) do |example, hash, existing_hash|
|
||||
name = example.fetch(:name)
|
||||
name = example.fetch(:name).to_sym
|
||||
example_statuses = glfm_examples_statuses[name] || {}
|
||||
|
||||
static = if example_statuses['skip_update_example_snapshot_html_static']
|
||||
existing_hash.dig(name, 'static')
|
||||
static = if example_statuses[:skip_update_example_snapshot_html_static]
|
||||
existing_hash.dig(name, :static)
|
||||
else
|
||||
static_html_hash[name]
|
||||
end
|
||||
|
||||
wysiwyg = if example_statuses['skip_update_example_snapshot_html_wysiwyg']
|
||||
existing_hash.dig(name, 'wysiwyg')
|
||||
wysiwyg = if example_statuses[:skip_update_example_snapshot_html_wysiwyg]
|
||||
existing_hash.dig(name, :wysiwyg)
|
||||
else
|
||||
wysiwyg_html_and_json_hash.dig(name, 'html')
|
||||
wysiwyg_html_and_json_hash.dig(name, :html)
|
||||
end
|
||||
|
||||
hash[name] = {
|
||||
|
@ -273,14 +275,14 @@ module Glfm
|
|||
|
||||
def write_prosemirror_json_yml(all_examples, wysiwyg_html_and_json_hash, glfm_examples_statuses)
|
||||
generate_and_write_for_all_examples(
|
||||
all_examples, ES_PROSEMIRROR_JSON_YML_PATH, glfm_examples_statuses
|
||||
all_examples, ES_PROSEMIRROR_JSON_YML_PATH, glfm_examples_statuses: glfm_examples_statuses
|
||||
) do |example, hash, existing_hash|
|
||||
name = example.fetch(:name)
|
||||
name = example.fetch(:name).to_sym
|
||||
|
||||
json = if glfm_examples_statuses.dig(name, 'skip_update_example_snapshot_prosemirror_json')
|
||||
json = if glfm_examples_statuses.dig(name, :skip_update_example_snapshot_prosemirror_json)
|
||||
existing_hash[name]
|
||||
else
|
||||
wysiwyg_html_and_json_hash.dig(name, 'json')
|
||||
wysiwyg_html_and_json_hash.dig(name, :json)
|
||||
end
|
||||
|
||||
# Do not assign nil values
|
||||
|
@ -289,15 +291,15 @@ module Glfm
|
|||
end
|
||||
|
||||
def generate_and_write_for_all_examples(
|
||||
all_examples, output_file_path, glfm_examples_statuses = {}, literal_scalars: true
|
||||
all_examples, output_file_path, glfm_examples_statuses: {}, literal_scalars: true
|
||||
)
|
||||
preserve_existing = !glfm_examples_statuses.empty?
|
||||
output("#{preserve_existing ? 'Creating/Updating' : 'Creating/Overwriting'} #{output_file_path}...")
|
||||
existing_hash = preserve_existing ? YAML.safe_load(File.open(output_file_path)) : {}
|
||||
existing_hash = preserve_existing ? YAML.safe_load(File.open(output_file_path), symbolize_names: true) : {}
|
||||
|
||||
output_hash = all_examples.each_with_object({}) do |example, hash|
|
||||
name = example.fetch(:name)
|
||||
if (reason = glfm_examples_statuses.dig(name, 'skip_update_example_snapshots'))
|
||||
name = example.fetch(:name).to_sym
|
||||
if (reason = glfm_examples_statuses.dig(name, :skip_update_example_snapshots))
|
||||
# Output the reason for skipping the example, but only once, not multiple times for each file
|
||||
output("Skipping '#{name}'. Reason: #{reason}") unless glfm_examples_statuses.dig(name, :already_printed)
|
||||
# We just store the `:already_printed` flag in the hash entry itself. Then we
|
||||
|
@ -317,37 +319,5 @@ module Glfm
|
|||
yaml_string = dump_yaml_with_formatting(output_hash, literal_scalars: literal_scalars)
|
||||
write_file(output_file_path, yaml_string)
|
||||
end
|
||||
|
||||
# Construct an AST so we can control YAML formatting for
|
||||
# YAML block scalar literals and key quoting.
|
||||
#
|
||||
# Note that when Psych dumps the markdown to YAML, it will
|
||||
# automatically use the default "clip" behavior of the Block Chomping Indicator (`|`)
|
||||
# https://yaml.org/spec/1.2.2/#8112-block-chomping-indicator,
|
||||
# when the markdown strings contain a trailing newline. The type of
|
||||
# Block Chomping Indicator is automatically determined, you cannot specify it
|
||||
# manually.
|
||||
def dump_yaml_with_formatting(hash, literal_scalars:)
|
||||
visitor = Psych::Visitors::YAMLTree.create
|
||||
visitor << hash
|
||||
ast = visitor.tree
|
||||
|
||||
# Force all scalars to have literal formatting (using Block Chomping Indicator instead of quotes)
|
||||
if literal_scalars
|
||||
ast.grep(Psych::Nodes::Scalar).each do |node|
|
||||
node.style = Psych::Nodes::Scalar::LITERAL
|
||||
end
|
||||
end
|
||||
|
||||
# Do not quote the keys
|
||||
ast.grep(Psych::Nodes::Mapping).each do |node|
|
||||
node.children.each_slice(2) do |k, _|
|
||||
k.quoted = false
|
||||
k.style = Psych::Nodes::Scalar::ANY
|
||||
end
|
||||
end
|
||||
|
||||
ast.to_yaml
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,7 +41,39 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when delete event is clicked' do
|
||||
context 'when edit is clicked' do
|
||||
before do
|
||||
click_button 'Add new timeline event'
|
||||
fill_in 'Description', with: 'Event note to edit'
|
||||
click_button 'Save'
|
||||
end
|
||||
|
||||
it 'shows the confirmation modal and edits the event' do
|
||||
click_button 'More actions'
|
||||
|
||||
page.within '.gl-new-dropdown-contents' do
|
||||
expect(page).to have_content('Edit')
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: 'Edit').click
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.common-note-form')
|
||||
|
||||
fill_in 'Description', with: 'Event note goes here'
|
||||
fill_in 'timeline-input-hours', with: '07'
|
||||
fill_in 'timeline-input-minutes', with: '25'
|
||||
|
||||
click_button 'Save'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
page.within '.timeline-event-note' do
|
||||
expect(page).to have_content('Event note goes here')
|
||||
expect(page).to have_content('07:25')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when delete is clicked' do
|
||||
before do
|
||||
click_button 'Add new timeline event'
|
||||
fill_in 'Description', with: 'Event note to delete'
|
||||
|
@ -51,7 +83,7 @@ RSpec.describe 'Incident timeline events', :js do
|
|||
it 'shows the confirmation modal and deletes the event' do
|
||||
click_button 'More actions'
|
||||
|
||||
page.within '.gl-new-dropdown-item-text-wrapper' do
|
||||
page.within '.gl-new-dropdown-contents' do
|
||||
expect(page).to have_content('Delete')
|
||||
page.find('.gl-new-dropdown-item-text-primary', text: 'Delete').click
|
||||
end
|
||||
|
|
|
@ -2,15 +2,15 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import { dispatch } from 'codesandbox-api';
|
||||
import smooshpack from 'smooshpack';
|
||||
import { SandpackClient } from '@codesandbox/sandpack-client';
|
||||
import Vuex from 'vuex';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import Clientside from '~/ide/components/preview/clientside.vue';
|
||||
import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
|
||||
import eventHub from '~/ide/eventhub';
|
||||
|
||||
jest.mock('smooshpack', () => ({
|
||||
Manager: jest.fn(),
|
||||
jest.mock('@codesandbox/sandpack-client', () => ({
|
||||
SandpackClient: jest.fn(),
|
||||
}));
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -78,8 +78,8 @@ describe('IDE clientside preview', () => {
|
|||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
sandpackReady: true,
|
||||
manager: {
|
||||
listener: jest.fn(),
|
||||
client: {
|
||||
cleanup: jest.fn(),
|
||||
updatePreview: jest.fn(),
|
||||
},
|
||||
});
|
||||
|
@ -90,9 +90,9 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
describe('without main entry', () => {
|
||||
it('creates sandpack manager', () => {
|
||||
it('creates sandpack client', () => {
|
||||
createComponent();
|
||||
expect(smooshpack.Manager).not.toHaveBeenCalled();
|
||||
expect(SandpackClient).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('with main entry', () => {
|
||||
|
@ -102,8 +102,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith(
|
||||
it('creates sandpack client', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith(
|
||||
'#ide-preview',
|
||||
expectedSandpackOptions(),
|
||||
expectedSandpackSettings(),
|
||||
|
@ -141,8 +141,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager with bundlerURL', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
|
||||
it('creates sandpack client with bundlerURL', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
|
||||
...expectedSandpackSettings(),
|
||||
bundlerURL: TEST_BUNDLER_URL,
|
||||
});
|
||||
|
@ -156,8 +156,8 @@ describe('IDE clientside preview', () => {
|
|||
return waitForPromises();
|
||||
});
|
||||
|
||||
it('creates sandpack manager', () => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalledWith(
|
||||
it('creates sandpack client', () => {
|
||||
expect(SandpackClient).toHaveBeenCalledWith(
|
||||
'#ide-preview',
|
||||
{
|
||||
files: {},
|
||||
|
@ -332,7 +332,7 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('initializes manager if manager is empty', () => {
|
||||
it('initializes client if client is empty', () => {
|
||||
createComponent({ getters: { packageJson: dummyPackageJson } });
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
|
@ -340,7 +340,7 @@ describe('IDE clientside preview', () => {
|
|||
wrapper.vm.update();
|
||||
|
||||
return waitForPromises().then(() => {
|
||||
expect(smooshpack.Manager).toHaveBeenCalled();
|
||||
expect(SandpackClient).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -349,7 +349,7 @@ describe('IDE clientside preview', () => {
|
|||
|
||||
wrapper.vm.update();
|
||||
|
||||
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -361,7 +361,7 @@ describe('IDE clientside preview', () => {
|
|||
});
|
||||
|
||||
it('calls updatePreview', () => {
|
||||
expect(wrapper.vm.manager.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -405,7 +405,7 @@ describe('IDE clientside preview', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
createInitializedComponent();
|
||||
spy = wrapper.vm.manager.updatePreview;
|
||||
spy = wrapper.vm.client.updatePreview;
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ jest.mock('codesandbox-api', () => ({
|
|||
|
||||
describe('IDE clientside preview navigator', () => {
|
||||
let wrapper;
|
||||
let manager;
|
||||
let client;
|
||||
let listenHandler;
|
||||
|
||||
const findBackButton = () => wrapper.findAll('button').at(0);
|
||||
|
@ -20,9 +20,9 @@ describe('IDE clientside preview navigator', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
listen.mockClear();
|
||||
manager = { bundlerURL: TEST_HOST, iframe: { src: '' } };
|
||||
client = { bundlerURL: TEST_HOST, iframe: { src: '' } };
|
||||
|
||||
wrapper = shallowMount(ClientsideNavigator, { propsData: { manager } });
|
||||
wrapper = shallowMount(ClientsideNavigator, { propsData: { client } });
|
||||
[[listenHandler]] = listen.mock.calls;
|
||||
});
|
||||
|
||||
|
@ -31,7 +31,7 @@ describe('IDE clientside preview navigator', () => {
|
|||
});
|
||||
|
||||
it('renders readonly URL bar', async () => {
|
||||
listenHandler({ type: 'urlchange', url: manager.bundlerURL });
|
||||
listenHandler({ type: 'urlchange', url: client.bundlerURL });
|
||||
await nextTick();
|
||||
expect(wrapper.find('input[readonly]').element.value).toBe('/');
|
||||
});
|
||||
|
@ -89,13 +89,13 @@ describe('IDE clientside preview navigator', () => {
|
|||
expect(findBackButton().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('updates manager iframe src', async () => {
|
||||
it('updates client iframe src', async () => {
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
|
||||
await nextTick();
|
||||
findBackButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -133,13 +133,13 @@ describe('IDE clientside preview navigator', () => {
|
|||
expect(findForwardButton().attributes('disabled')).toBe('disabled');
|
||||
});
|
||||
|
||||
it('updates manager iframe src', async () => {
|
||||
it('updates client iframe src', async () => {
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
|
||||
listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
|
||||
await nextTick();
|
||||
findBackButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -152,10 +152,10 @@ describe('IDE clientside preview navigator', () => {
|
|||
});
|
||||
|
||||
it('calls refresh with current path', () => {
|
||||
manager.iframe.src = 'something-other';
|
||||
client.iframe.src = 'something-other';
|
||||
findRefreshButton().trigger('click');
|
||||
|
||||
expect(manager.iframe.src).toBe(url);
|
||||
expect(client.iframe.src).toBe(url);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -42,17 +42,15 @@ describe('Create Timeline events', () => {
|
|||
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
|
||||
const findNoteInput = () => wrapper.findByTestId('input-note');
|
||||
const setNoteInput = () => {
|
||||
const textarea = findNoteInput().element;
|
||||
textarea.value = mockInputData.note;
|
||||
textarea.dispatchEvent(new Event('input'));
|
||||
findNoteInput().setValue(mockInputData.note);
|
||||
};
|
||||
const findHourInput = () => wrapper.findByTestId('input-hours');
|
||||
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
|
||||
const setDatetime = () => {
|
||||
const inputDate = new Date(mockInputData.occurredAt);
|
||||
findDatePicker().vm.$emit('input', inputDate);
|
||||
findHourInput().vm.$emit('input', inputDate.getHours());
|
||||
findMinuteInput().vm.$emit('input', inputDate.getMinutes());
|
||||
findHourInput().setValue(inputDate.getHours());
|
||||
findMinuteInput().setValue(inputDate.getMinutes());
|
||||
};
|
||||
const fillForm = () => {
|
||||
setDatetime();
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
|
||||
|
||||
import { mockEvents, fakeEventData, mockInputData } from './mock_data';
|
||||
|
||||
describe('Edit Timeline events', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(EditTimelineEvent, {
|
||||
propsData: {
|
||||
event: mockEvents[0],
|
||||
editTimelineEventActive: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm);
|
||||
|
||||
const mockSaveData = { ...fakeEventData, ...mockInputData };
|
||||
|
||||
describe('editTimelineEvent', () => {
|
||||
const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] };
|
||||
|
||||
it('should call the mutation with the right variables', async () => {
|
||||
await findTimelineEventsForm().vm.$emit('save-event', mockSaveData, false);
|
||||
|
||||
expect(wrapper.emitted()).toEqual(saveEventEvent);
|
||||
});
|
||||
|
||||
it('should close the form on cancel', async () => {
|
||||
const cancelEvent = { 'hide-edit': [[]] };
|
||||
|
||||
await findTimelineEventsForm().vm.$emit('cancel');
|
||||
|
||||
expect(wrapper.emitted()).toEqual(cancelEvent);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -49,6 +49,15 @@ export const mockEvents = [
|
|||
},
|
||||
];
|
||||
|
||||
const mockUpdatedEvent = {
|
||||
id: 'gid://gitlab/IncidentManagement::TimelineEvent/8',
|
||||
note: 'another one23',
|
||||
noteHtml: '<p>another one23</p>',
|
||||
action: 'comment',
|
||||
occurredAt: '2022-07-01T12:47:00Z',
|
||||
createdAt: '2022-07-20T12:47:40Z',
|
||||
};
|
||||
|
||||
export const timelineEventsQueryListResponse = {
|
||||
data: {
|
||||
project: {
|
||||
|
@ -93,6 +102,29 @@ export const timelineEventsCreateEventError = {
|
|||
},
|
||||
};
|
||||
|
||||
export const timelineEventsEditEventResponse = {
|
||||
data: {
|
||||
timelineEventUpdate: {
|
||||
timelineEvent: {
|
||||
...mockUpdatedEvent,
|
||||
},
|
||||
errors: [],
|
||||
__typename: 'TimelineEventUpdatePayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const timelineEventsEditEventError = {
|
||||
data: {
|
||||
timelineEventUpdate: {
|
||||
timelineEvent: {
|
||||
...mockUpdatedEvent,
|
||||
},
|
||||
errors: ['Create error'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const timelineEventDeleteData = (errors = []) => {
|
||||
return {
|
||||
data: {
|
||||
|
@ -128,5 +160,10 @@ export const mockGetTimelineData = {
|
|||
|
||||
export const fakeDate = '2020-07-08T00:00:00.000Z';
|
||||
|
||||
export const mockInputData = {
|
||||
note: 'test',
|
||||
occurredAt: '2020-08-10T02:30:00.000Z',
|
||||
};
|
||||
|
||||
const { id, note, occurredAt } = mockEvents[0];
|
||||
export const fakeEventData = { id, note, occurredAt };
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Timeline events form', () => {
|
|||
const mountComponent = ({ mountMethod = shallowMountExtended }) => {
|
||||
wrapper = mountMethod(TimelineEventsForm, {
|
||||
propsData: {
|
||||
hasTimelineEvents: true,
|
||||
showSaveAndAdd: true,
|
||||
isEventProcessed: false,
|
||||
},
|
||||
});
|
||||
|
@ -42,8 +42,8 @@ describe('Timeline events form', () => {
|
|||
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
|
||||
const setDatetime = () => {
|
||||
findDatePicker().vm.$emit('input', mockInputDate);
|
||||
findHourInput().vm.$emit('input', 5);
|
||||
findMinuteInput().vm.$emit('input', 45);
|
||||
findHourInput().setValue(5);
|
||||
findMinuteInput().setValue(45);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
|
|
|
@ -15,7 +15,6 @@ describe('IncidentTimelineEventList', () => {
|
|||
action,
|
||||
noteHtml,
|
||||
occurredAt,
|
||||
isLastItem: false,
|
||||
...propsData,
|
||||
},
|
||||
provide: {
|
||||
|
@ -26,7 +25,6 @@ describe('IncidentTimelineEventList', () => {
|
|||
};
|
||||
|
||||
const findCommentIcon = () => wrapper.findComponent(GlIcon);
|
||||
const findTextContainer = () => wrapper.findByTestId('event-text-container');
|
||||
const findEventTime = () => wrapper.findByTestId('event-time');
|
||||
const findDropdown = () => wrapper.findComponent(GlDropdown);
|
||||
const findDeleteButton = () => wrapper.findByText('Delete');
|
||||
|
@ -50,20 +48,6 @@ describe('IncidentTimelineEventList', () => {
|
|||
expect(findEventTime().text()).toBe('15:59 UTC');
|
||||
});
|
||||
|
||||
describe('last item in list', () => {
|
||||
it('shows a bottom border when not the last item', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTextContainer().classes()).toContain('gl-border-1');
|
||||
});
|
||||
|
||||
it('does not show a bottom border when the last item', () => {
|
||||
mountComponent({ propsData: { isLastItem: true } });
|
||||
|
||||
expect(wrapper.classes()).not.toContain('gl-border-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
timezone
|
||||
${'Europe/London'}
|
||||
|
|
|
@ -4,10 +4,11 @@ import Vue from 'vue';
|
|||
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
|
||||
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
|
||||
import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
|
||||
import EditTimelineEvent from '~/issues/show/components/incidents/edit_timeline_event.vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql';
|
||||
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
|
||||
import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { createAlert } from '~/flash';
|
||||
|
@ -15,9 +16,11 @@ import {
|
|||
mockEvents,
|
||||
timelineEventsDeleteEventResponse,
|
||||
timelineEventsDeleteEventError,
|
||||
timelineEventsEditEventResponse,
|
||||
timelineEventsEditEventError,
|
||||
fakeDate,
|
||||
fakeEventData,
|
||||
timelineEventsQueryListResponse,
|
||||
mockInputData,
|
||||
} from './mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
@ -32,20 +35,15 @@ const mockConfirmAction = ({ confirmed }) => {
|
|||
describe('IncidentTimelineEventList', () => {
|
||||
useFakeDate(fakeDate);
|
||||
let wrapper;
|
||||
const responseSpy = jest.fn().mockResolvedValue(timelineEventsDeleteEventResponse);
|
||||
const deleteResponseSpy = jest.fn().mockResolvedValue(timelineEventsDeleteEventResponse);
|
||||
const editResponseSpy = jest.fn().mockResolvedValue(timelineEventsEditEventResponse);
|
||||
|
||||
const requestHandlers = [[deleteTimelineEventMutation, responseSpy]];
|
||||
const requestHandlers = [
|
||||
[deleteTimelineEventMutation, deleteResponseSpy],
|
||||
[editTimelineEventMutation, editResponseSpy],
|
||||
];
|
||||
const apolloProvider = createMockApollo(requestHandlers);
|
||||
|
||||
apolloProvider.clients.defaultClient.cache.writeQuery({
|
||||
query: getTimelineEvents,
|
||||
data: timelineEventsQueryListResponse.data,
|
||||
variables: {
|
||||
fullPath: 'group/project',
|
||||
incidentId: 'gid://gitlab/Issue/1',
|
||||
},
|
||||
});
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(IncidentTimelineEventList, {
|
||||
propsData: {
|
||||
|
@ -70,6 +68,10 @@ describe('IncidentTimelineEventList', () => {
|
|||
await waitForPromises();
|
||||
};
|
||||
|
||||
const clickFirstEditButton = async () => {
|
||||
findItems().at(0).vm.$emit('edit');
|
||||
await waitForPromises();
|
||||
};
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
@ -86,12 +88,6 @@ describe('IncidentTimelineEventList', () => {
|
|||
expect(findItems(findSecondTimelineEventGroup())).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('sets the isLastItem prop correctly', () => {
|
||||
expect(findItems().at(0).props('isLastItem')).toBe(false);
|
||||
expect(findItems().at(1).props('isLastItem')).toBe(false);
|
||||
expect(findItems().at(2).props('isLastItem')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the event props correctly', () => {
|
||||
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
|
||||
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
|
||||
|
@ -133,7 +129,7 @@ describe('IncidentTimelineEventList', () => {
|
|||
const expectedVars = { input: { id: mockEvents[0].id } };
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
expect(responseSpy).toHaveBeenCalledWith(expectedVars);
|
||||
expect(deleteResponseSpy).toHaveBeenCalledWith(expectedVars);
|
||||
});
|
||||
|
||||
it('should show an error when delete returns an error', async () => {
|
||||
|
@ -141,7 +137,7 @@ describe('IncidentTimelineEventList', () => {
|
|||
message: 'Error deleting incident timeline event: Item does not exist',
|
||||
};
|
||||
|
||||
responseSpy.mockResolvedValue(timelineEventsDeleteEventError);
|
||||
deleteResponseSpy.mockResolvedValue(timelineEventsDeleteEventError);
|
||||
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
|
@ -154,7 +150,7 @@ describe('IncidentTimelineEventList', () => {
|
|||
error: new Error(),
|
||||
message: 'Something went wrong while deleting the incident timeline event.',
|
||||
};
|
||||
responseSpy.mockRejectedValueOnce();
|
||||
deleteResponseSpy.mockRejectedValueOnce();
|
||||
|
||||
await clickFirstDeleteButton();
|
||||
|
||||
|
@ -162,4 +158,76 @@ describe('IncidentTimelineEventList', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Functionality', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
clickFirstEditButton();
|
||||
});
|
||||
|
||||
const findEditEvent = () => wrapper.findComponent(EditTimelineEvent);
|
||||
const mockSaveData = { ...fakeEventData, ...mockInputData };
|
||||
|
||||
describe('editTimelineEvent', () => {
|
||||
it('should call the mutation with the right variables', async () => {
|
||||
await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
|
||||
await waitForPromises();
|
||||
|
||||
expect(editResponseSpy).toHaveBeenCalledWith({
|
||||
input: mockSaveData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should close the form on successful addition', async () => {
|
||||
await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findEditEvent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should close the form on cancel', async () => {
|
||||
await findEditEvent().vm.$emit('hide-edit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findEditEvent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should show an error when submission returns an error', async () => {
|
||||
const expectedAlertArgs = {
|
||||
message: `Error updating incident timeline event: ${timelineEventsEditEventError.data.timelineEventUpdate.errors[0]}`,
|
||||
};
|
||||
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
|
||||
|
||||
await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||
});
|
||||
|
||||
it('should show an error when submission fails', async () => {
|
||||
const expectedAlertArgs = {
|
||||
captureError: true,
|
||||
error: new Error(),
|
||||
message: 'Something went wrong while updating the incident timeline event.',
|
||||
};
|
||||
editResponseSpy.mockRejectedValueOnce();
|
||||
|
||||
await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
|
||||
await waitForPromises();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
|
||||
});
|
||||
|
||||
it('should keep the form open on failed addition', async () => {
|
||||
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
|
||||
|
||||
await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findEditEvent().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -143,22 +143,13 @@ describe('TimelineEventsTab', () => {
|
|||
});
|
||||
|
||||
it('should not show a form by default', () => {
|
||||
expect(findCreateTimelineEvent().isVisible()).toBe(false);
|
||||
expect(findCreateTimelineEvent().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should show a form when button is clicked', async () => {
|
||||
await findAddEventButton().trigger('click');
|
||||
|
||||
expect(findCreateTimelineEvent().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should clear the form when button is clicked', async () => {
|
||||
const mockClear = jest.fn();
|
||||
wrapper.vm.$refs.createEventForm.clearForm = mockClear;
|
||||
|
||||
await findAddEventButton().trigger('click');
|
||||
|
||||
expect(mockClear).toHaveBeenCalled();
|
||||
expect(findCreateTimelineEvent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide the form when the hide event is emitted', async () => {
|
||||
|
@ -167,7 +158,7 @@ describe('TimelineEventsTab', () => {
|
|||
|
||||
await findCreateTimelineEvent().vm.$emit('hide-new-timeline-events-form');
|
||||
|
||||
expect(findCreateTimelineEvent().isVisible()).toBe(false);
|
||||
expect(findCreateTimelineEvent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock';
|
|||
import {
|
||||
displayAndLogError,
|
||||
getEventIcon,
|
||||
getUtcShiftedDateNow,
|
||||
getUtcShiftedDate,
|
||||
} from '~/issues/show/components/incidents/utils';
|
||||
import { createAlert } from '~/flash';
|
||||
|
||||
|
@ -34,7 +34,7 @@ describe('incident utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getUtcShiftedDateNow', () => {
|
||||
describe('getUtcShiftedDate', () => {
|
||||
beforeEach(() => {
|
||||
timezoneMock.register('US/Pacific');
|
||||
});
|
||||
|
@ -46,7 +46,7 @@ describe('incident utils', () => {
|
|||
it('should shift the date by the timezone offset', () => {
|
||||
const date = new Date();
|
||||
|
||||
const shiftedDate = getUtcShiftedDateNow();
|
||||
const shiftedDate = getUtcShiftedDate();
|
||||
|
||||
expect(shiftedDate > date).toBe(true);
|
||||
});
|
||||
|
|
|
@ -38,7 +38,7 @@ export default [
|
|||
'</tr>\n',
|
||||
'</table>',
|
||||
].join(''),
|
||||
output: '<table>',
|
||||
output: '<table data-myattr="XSS">',
|
||||
},
|
||||
],
|
||||
// Note: style is sanitized out
|
||||
|
@ -98,7 +98,7 @@ export default [
|
|||
'</svg>',
|
||||
].join(),
|
||||
output:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="388.84pt" version="1.0" id="svg2" height="115.02pt">',
|
||||
'<svg height="115.02pt" id="svg2" version="1.0" width="388.84pt" xmlns="http://www.w3.org/2000/svg">',
|
||||
},
|
||||
],
|
||||
];
|
||||
|
|
|
@ -49,15 +49,17 @@ describe('Output component', () => {
|
|||
const htmlType = json.cells[4];
|
||||
createComponent(htmlType.outputs[0]);
|
||||
|
||||
expect(wrapper.findAll('p')).toHaveLength(1);
|
||||
expect(wrapper.text()).toContain('test');
|
||||
const iframe = wrapper.find('iframe');
|
||||
expect(iframe.exists()).toBe(true);
|
||||
expect(iframe.element.getAttribute('sandbox')).toBe('');
|
||||
expect(iframe.element.getAttribute('srcdoc')).toBe('<p>test</p>');
|
||||
});
|
||||
|
||||
it('renders multiple raw HTML outputs', () => {
|
||||
const htmlType = json.cells[4];
|
||||
createComponent([htmlType.outputs[0], htmlType.outputs[0]]);
|
||||
|
||||
expect(wrapper.findAll('p')).toHaveLength(2);
|
||||
expect(wrapper.findAll('iframe')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,7 +86,11 @@ describe('Output component', () => {
|
|||
});
|
||||
|
||||
it('renders as an svg', () => {
|
||||
expect(wrapper.find('svg').exists()).toBe(true);
|
||||
const iframe = wrapper.find('iframe');
|
||||
|
||||
expect(iframe.exists()).toBe(true);
|
||||
expect(iframe.element.getAttribute('sandbox')).toBe('');
|
||||
expect(iframe.element.getAttribute('srcdoc')).toBe('<svg></svg>');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -320,7 +320,7 @@ RSpec.describe CommitsHelper do
|
|||
let(:current_path) { "test" }
|
||||
|
||||
before do
|
||||
expect(commit).to receive(:status_for).with(ref).and_return(commit_status)
|
||||
expect(commit).to receive(:detailed_status_for).with(ref).and_return(commit_status)
|
||||
assign(:path, current_path)
|
||||
end
|
||||
|
||||
|
|
|
@ -112,6 +112,14 @@ RSpec.describe LabelsHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'render_label_text' do
|
||||
it 'html escapes the bg_color correctly' do
|
||||
xss_payload = '"><img src=x onerror=prompt(1)>'
|
||||
label_text = render_label_text('xss', bg_color: xss_payload)
|
||||
expect(label_text).to include(html_escape(xss_payload))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'text_color_for_bg' do
|
||||
it 'uses light text on dark backgrounds' do
|
||||
expect(text_color_for_bg('#222E2E')).to be_color('#FFFFFF')
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Rack VULNDB-255039' do
|
||||
context 'when handling query params in GET requests' do
|
||||
it 'does not treat semicolons as query delimiters' do
|
||||
env = ::Rack::MockRequest.env_for('http://gitlab.com?a=b;c=1')
|
||||
|
||||
query_hash = ::Rack::Request.new(env).GET
|
||||
|
||||
# Prior to this patch, this was splitting around the semicolon, which
|
||||
# would return {"a"=>"b", "c"=>"1"}
|
||||
expect(query_hash).to eq({ "a" => "b;c=1" })
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,69 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
require 'sawyer'
|
||||
|
||||
require_relative '../../config/initializers/sawyer_patch'
|
||||
|
||||
RSpec.describe 'sawyer_patch' do
|
||||
it 'raises error when acessing a method that overlaps a Ruby method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
to_s: 'Overriding method',
|
||||
user: { to_s: 'Overriding method', name: 'User name' }
|
||||
}
|
||||
)
|
||||
|
||||
error_message = 'Sawyer method "to_s" overlaps Ruby method. Convert to a hash to access the attribute.'
|
||||
expect { sawyer_resource.to_s }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.to_s? }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.to_s = 'new value' }.to raise_error(Sawyer::Error, error_message)
|
||||
expect { sawyer_resource.user.to_s }.to raise_error(Sawyer::Error, error_message)
|
||||
expect(sawyer_resource.user.name).to eq('User name')
|
||||
end
|
||||
|
||||
it 'raises error when acessing a boolean method that overlaps a Ruby method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
nil?: 'value'
|
||||
}
|
||||
)
|
||||
|
||||
expect { sawyer_resource.nil? }.to raise_error(Sawyer::Error)
|
||||
end
|
||||
|
||||
it 'raises error when acessing a method that expects an argument' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
'user': 'value',
|
||||
'user=': 'value',
|
||||
'==': 'value',
|
||||
'!=': 'value',
|
||||
'+': 'value'
|
||||
}
|
||||
)
|
||||
|
||||
expect(sawyer_resource.user).to eq('value')
|
||||
expect { sawyer_resource.user = 'New user' }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource == true }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource != true }.to raise_error(ArgumentError)
|
||||
expect { sawyer_resource + 1 }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'does not raise error if is not an overlapping method' do
|
||||
sawyer_resource = Sawyer::Resource.new(
|
||||
Sawyer::Agent.new(''),
|
||||
{
|
||||
count_total: 1,
|
||||
user: { name: 'User name' }
|
||||
}
|
||||
)
|
||||
|
||||
expect(sawyer_resource.count_total).to eq(1)
|
||||
expect(sawyer_resource.count_total?).to eq(true)
|
||||
expect(sawyer_resource.count_total + 1).to eq(2)
|
||||
expect(sawyer_resource.user.name).to eq('User name')
|
||||
end
|
||||
end
|
|
@ -18,10 +18,20 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
|
|||
context 'detects' do
|
||||
let(:email) { FFaker::Internet.email }
|
||||
|
||||
it 'trailers in the form of *-by and replace users with links' do
|
||||
doc = filter(commit_message_html)
|
||||
context 'trailers in the form of *-by' do
|
||||
where(:commit_trailer) do
|
||||
["#{FFaker::Lorem.word}-by:", "#{FFaker::Lorem.word}-BY:", "#{FFaker::Lorem.word}-By:"]
|
||||
end
|
||||
|
||||
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
|
||||
with_them do
|
||||
let(:trailer) { commit_trailer }
|
||||
|
||||
it 'replaces users with links' do
|
||||
doc = filter(commit_message_html)
|
||||
|
||||
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'trailers prefixed with whitespaces' do
|
||||
|
@ -121,7 +131,7 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter do
|
|||
|
||||
context "ignores" do
|
||||
it 'commit messages without trailers' do
|
||||
exp = message = commit_html(FFaker::Lorem.sentence)
|
||||
exp = message = commit_html(Array.new(5) { FFaker::Lorem.sentence }.join("\n"))
|
||||
doc = filter(message)
|
||||
|
||||
expect(doc.to_html).to match Regexp.escape(exp)
|
||||
|
|
|
@ -92,5 +92,50 @@ RSpec.describe Banzai::Filter::ImageLinkFilter do
|
|||
|
||||
expect(doc.at_css('a')['class']).to match(%r{with-attachment-icon})
|
||||
end
|
||||
|
||||
context 'when link attributes contain malicious code' do
|
||||
let(:malicious_code) do
|
||||
# rubocop:disable Layout/LineLength
|
||||
%q(<a class='fixed-top fixed-bottom' data-create-path=/malicious-url><style> .tab-content>.tab-pane{display: block !important}</style>)
|
||||
# rubocop:enable Layout/LineLength
|
||||
end
|
||||
|
||||
context 'when image alt contains malicious code' do
|
||||
it 'ignores image alt and uses image path as the link text', :aggregate_failures do
|
||||
doc = filter(image(path, alt: malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
|
||||
expect(doc.at_css('a')['href']).to eq(path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when image src contains malicious code' do
|
||||
it 'ignores image src and does not use it as the link text' do
|
||||
doc = filter(image(malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*></a>$})
|
||||
end
|
||||
|
||||
it 'keeps image src unchanged, malicious code does not execute as part of url' do
|
||||
doc = filter(image(malicious_code), context)
|
||||
|
||||
expect(doc.at_css('a')['href']).to eq(malicious_code)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when image data-src contains malicious code' do
|
||||
it 'ignores data-src and uses image path as the link text', :aggregate_failures do
|
||||
doc = filter(image(path, data_src: malicious_code), context)
|
||||
|
||||
expect(doc.to_html).to match(%r{^<a[^>]*>#{path}</a>$})
|
||||
end
|
||||
|
||||
it 'uses image data-src, malicious code does not execute as part of url' do
|
||||
doc = filter(image(path, data_src: malicious_code), context)
|
||||
|
||||
expect(doc.at_css('a')['href']).to eq(malicious_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::Filter::PathologicalMarkdownFilter do
|
||||
include FilterSpecHelper
|
||||
|
||||
let_it_be(:short_text) { '![a' * 5 }
|
||||
let_it_be(:long_text) { ([short_text] * 10).join(' ') }
|
||||
let_it_be(:with_images_text) { "![One ![one](one.jpg) #{'and\n' * 200} ![two ![two](two.jpg)" }
|
||||
|
||||
it 'detects a significat number of unclosed image links' do
|
||||
msg = <<~TEXT
|
||||
_Unable to render markdown - too many unclosed markdown image links detected._
|
||||
TEXT
|
||||
|
||||
expect(filter(long_text)).to eq(msg.strip)
|
||||
end
|
||||
|
||||
it 'does nothing when there are only a few unclosed image links' do
|
||||
expect(filter(short_text)).to eq(short_text)
|
||||
end
|
||||
|
||||
it 'does nothing when there are only a few unclosed image links and images' do
|
||||
expect(filter(with_images_text)).to eq(with_images_text)
|
||||
end
|
||||
end
|
|
@ -167,4 +167,16 @@ RSpec.describe Banzai::Pipeline::FullPipeline do
|
|||
expect(output).to include('<em>@test_</em>')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unclosed image links' do
|
||||
it 'detects a significat number of unclosed image links' do
|
||||
markdown = '![a ' * 30
|
||||
msg = <<~TEXT
|
||||
Unable to render markdown - too many unclosed markdown image links detected.
|
||||
TEXT
|
||||
output = described_class.to_html(markdown, project: nil)
|
||||
|
||||
expect(output).to include(msg.strip)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,12 +9,13 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:repository) { project.repository.raw }
|
||||
|
||||
shared_examples :repo do
|
||||
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, pagination_params) }
|
||||
subject(:tree) { Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, pagination_params) }
|
||||
|
||||
let(:sha) { SeedRepo::Commit::ID }
|
||||
let(:path) { nil }
|
||||
let(:recursive) { false }
|
||||
let(:pagination_params) { nil }
|
||||
let(:skip_flat_paths) { false }
|
||||
|
||||
let(:entries) { tree.first }
|
||||
let(:cursor) { tree.second }
|
||||
|
@ -107,6 +108,12 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
end
|
||||
|
||||
it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') }
|
||||
|
||||
context 'when skip_flat_paths is true' do
|
||||
let(:skip_flat_paths) { true }
|
||||
|
||||
it { expect(subdir_file.flat_path).to be_blank }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -162,7 +169,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
allow(instance).to receive(:lookup).with(SeedRepo::Commit::ID)
|
||||
end
|
||||
|
||||
described_class.where(repository, SeedRepo::Commit::ID, 'files', false)
|
||||
described_class.where(repository, SeedRepo::Commit::ID, 'files', false, false)
|
||||
end
|
||||
|
||||
it_behaves_like :repo do
|
||||
|
@ -180,7 +187,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:entries_count) { entries.count }
|
||||
|
||||
it 'returns all entries without a cursor' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: entries_count, page_token: nil })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: entries_count, page_token: nil })
|
||||
|
||||
expect(cursor).to be_nil
|
||||
expect(result.entries.count).to eq(entries_count)
|
||||
|
@ -209,7 +216,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:entries_count) { entries.count }
|
||||
|
||||
it 'returns all entries' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: nil })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: nil })
|
||||
|
||||
expect(result.count).to eq(entries_count)
|
||||
expect(cursor).to be_nil
|
||||
|
@ -220,7 +227,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
let(:token) { entries.second.id }
|
||||
|
||||
it 'returns all entries after token' do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: -1, page_token: token })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: -1, page_token: token })
|
||||
|
||||
expect(result.count).to eq(entries.count - 2)
|
||||
expect(cursor).to be_nil
|
||||
|
@ -252,7 +259,7 @@ RSpec.describe Gitlab::Git::Tree do
|
|||
expected_entries = entries
|
||||
|
||||
loop do
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, { limit: 5, page_token: token })
|
||||
result, cursor = Gitlab::Git::Tree.where(repository, sha, path, recursive, skip_flat_paths, { limit: 5, page_token: token })
|
||||
|
||||
collected_entries += result.entries
|
||||
token = cursor&.next_cursor
|
||||
|
|
|
@ -150,16 +150,18 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
end
|
||||
|
||||
describe '#tree_entries' do
|
||||
subject { client.tree_entries(repository, revision, path, recursive, pagination_params) }
|
||||
subject { client.tree_entries(repository, revision, path, recursive, skip_flat_paths, pagination_params) }
|
||||
|
||||
let(:path) { '/' }
|
||||
let(:recursive) { false }
|
||||
let(:pagination_params) { nil }
|
||||
let(:skip_flat_paths) { false }
|
||||
|
||||
it 'sends a get_tree_entries message' do
|
||||
it 'sends a get_tree_entries message with default limit' do
|
||||
expected_pagination_params = Gitaly::PaginationParameter.new(limit: Gitlab::GitalyClient::CommitService::TREE_ENTRIES_DEFAULT_LIMIT)
|
||||
expect_any_instance_of(Gitaly::CommitService::Stub)
|
||||
.to receive(:get_tree_entries)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
|
||||
.and_return([])
|
||||
|
||||
is_expected.to eq([[], nil])
|
||||
|
@ -189,9 +191,10 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
|
|||
pagination_cursor: pagination_cursor
|
||||
)
|
||||
|
||||
expected_pagination_params = Gitaly::PaginationParameter.new(limit: 3)
|
||||
expect_any_instance_of(Gitaly::CommitService::Stub)
|
||||
.to receive(:get_tree_entries)
|
||||
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
|
||||
.with(gitaly_request_with_params({ pagination_params: expected_pagination_params }), kind_of(Hash))
|
||||
.and_return([response])
|
||||
|
||||
is_expected.to eq([[], pagination_cursor])
|
||||
|
|
|
@ -72,4 +72,18 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
|
|||
it { is_expected.to be(true) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'count' do
|
||||
subject { cache.count(cache_prefix) }
|
||||
|
||||
it { is_expected.to be(0) }
|
||||
|
||||
context 'item added' do
|
||||
before do
|
||||
cache.write(cache_prefix, 'test_item')
|
||||
end
|
||||
|
||||
it { is_expected.to be(1) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Zentao::Client do
|
||||
subject(:integration) { described_class.new(zentao_integration) }
|
||||
RSpec.describe Gitlab::Zentao::Client, :clean_gitlab_redis_cache do
|
||||
subject(:client) { described_class.new(zentao_integration) }
|
||||
|
||||
let(:zentao_integration) { create(:zentao_integration) }
|
||||
|
||||
def mock_get_products_url
|
||||
integration.send(:url, "products/#{zentao_integration.zentao_product_xid}")
|
||||
client.send(:url, "products/#{zentao_integration.zentao_product_xid}")
|
||||
end
|
||||
|
||||
def mock_fetch_issues_url
|
||||
client.send(:url, "products/#{zentao_integration.zentao_product_xid}/issues")
|
||||
end
|
||||
|
||||
def mock_fetch_issue_url(issue_id)
|
||||
integration.send(:url, "issues/#{issue_id}")
|
||||
client.send(:url, "issues/#{issue_id}")
|
||||
end
|
||||
|
||||
let(:mock_headers) do
|
||||
|
@ -29,13 +33,13 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
let(:zentao_integration) { nil }
|
||||
|
||||
it 'raises ConfigError' do
|
||||
expect { integration }.to raise_error(described_class::ConfigError)
|
||||
expect { client }.to raise_error(described_class::ConfigError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'integration is provided' do
|
||||
it 'is initialized successfully' do
|
||||
expect { integration }.not_to raise_error
|
||||
expect { client }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -50,7 +54,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'fetches the product' do
|
||||
expect(integration.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
|
||||
expect(client.fetch_product(zentao_integration.zentao_product_xid)).to eq mock_response
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -62,8 +66,8 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
|
||||
it 'fetches the empty product' do
|
||||
expect do
|
||||
integration.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::Error, 'request error')
|
||||
client.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::RequestError)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -75,7 +79,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
|
||||
it 'fetches the empty product' do
|
||||
expect do
|
||||
integration.fetch_product(zentao_integration.zentao_product_xid)
|
||||
client.fetch_product(zentao_integration.zentao_product_xid)
|
||||
end.to raise_error(Gitlab::Zentao::Client::Error, 'invalid response format')
|
||||
end
|
||||
end
|
||||
|
@ -89,7 +93,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'responds with success' do
|
||||
expect(integration.ping[:success]).to eq true
|
||||
expect(client.ping[:success]).to eq true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -100,7 +104,69 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
end
|
||||
|
||||
it 'responds with unsuccess' do
|
||||
expect(integration.ping[:success]).to eq false
|
||||
expect(client.ping[:success]).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_issues' do
|
||||
let(:mock_response) { { 'issues' => [{ 'id' => 'story-1' }, { 'id' => 'bug-11' }] } }
|
||||
|
||||
before do
|
||||
WebMock.stub_request(:get, mock_fetch_issues_url)
|
||||
.with(mock_headers).to_return(status: 200, body: mock_response.to_json)
|
||||
end
|
||||
|
||||
it 'returns the response' do
|
||||
expect(client.fetch_issues).to eq(mock_response)
|
||||
end
|
||||
|
||||
describe 'marking the issues as seen in the product' do
|
||||
let(:cache) { ::Gitlab::SetCache.new }
|
||||
let(:cache_key) do
|
||||
[
|
||||
:zentao_product_issues,
|
||||
OpenSSL::Digest::SHA256.hexdigest(zentao_integration.client_url),
|
||||
zentao_integration.zentao_product_xid
|
||||
].join(':')
|
||||
end
|
||||
|
||||
it 'adds issue ids to the cache' do
|
||||
expect { client.fetch_issues }.to change { cache.read(cache_key) }
|
||||
.from(be_empty)
|
||||
.to match_array(%w[bug-11 story-1])
|
||||
end
|
||||
|
||||
it 'does not add issue ids to the cache if max set size has been reached' do
|
||||
cache.write(cache_key, %w[foo bar])
|
||||
stub_const("#{described_class}::CACHE_MAX_SET_SIZE", 1)
|
||||
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.read(cache_key)).to match_array(%w[foo bar])
|
||||
end
|
||||
|
||||
it 'does not duplicate issue ids in the cache' do
|
||||
client.fetch_issues
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.read(cache_key)).to match_array(%w[bug-11 story-1])
|
||||
end
|
||||
|
||||
it 'touches the cache ttl every time issues are fetched' do
|
||||
fresh_ttl = 1.month.to_i
|
||||
|
||||
freeze_time do
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
|
||||
end
|
||||
|
||||
travel_to(1.minute.from_now) do
|
||||
client.fetch_issues
|
||||
|
||||
expect(cache.ttl(cache_key)).to eq(fresh_ttl)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -109,9 +175,9 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'with invalid id' do
|
||||
let(:invalid_ids) { ['story', 'story-', '-', '123', ''] }
|
||||
|
||||
it 'returns empty object' do
|
||||
it 'raises Error' do
|
||||
invalid_ids.each do |id|
|
||||
expect { integration.fetch_issue(id) }
|
||||
expect { client.fetch_issue(id) }
|
||||
.to raise_error(Gitlab::Zentao::Client::Error, 'invalid issue id')
|
||||
end
|
||||
end
|
||||
|
@ -120,12 +186,31 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'with valid id' do
|
||||
let(:valid_ids) { %w[story-1 bug-23] }
|
||||
|
||||
it 'fetches current issue' do
|
||||
valid_ids.each do |id|
|
||||
WebMock.stub_request(:get, mock_fetch_issue_url(id))
|
||||
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
|
||||
context 'when issue has been seen on the index' do
|
||||
before do
|
||||
issues_body = { issues: valid_ids.map { { id: _1 } } }.to_json
|
||||
|
||||
expect(integration.fetch_issue(id).dig('issue', 'id')).to eq id
|
||||
WebMock.stub_request(:get, mock_fetch_issues_url)
|
||||
.with(mock_headers).to_return(status: 200, body: issues_body)
|
||||
|
||||
client.fetch_issues
|
||||
end
|
||||
|
||||
it 'fetches the issue' do
|
||||
valid_ids.each do |id|
|
||||
WebMock.stub_request(:get, mock_fetch_issue_url(id))
|
||||
.with(mock_headers).to_return(status: 200, body: { issue: { id: id } }.to_json)
|
||||
|
||||
expect(client.fetch_issue(id).dig('issue', 'id')).to eq id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue has not been seen on the index' do
|
||||
it 'raises RequestError' do
|
||||
valid_ids.each do |id|
|
||||
expect { client.fetch_issue(id) }.to raise_error(Gitlab::Zentao::Client::RequestError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -135,7 +220,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
context 'api url' do
|
||||
shared_examples 'joins api_url correctly' do
|
||||
it 'verify url' do
|
||||
expect(integration.send(:url, "products/1").to_s)
|
||||
expect(client.send(:url, "products/1").to_s)
|
||||
.to eq("https://jihudemo.zentao.net/zentao/api.php/v1/products/1")
|
||||
end
|
||||
end
|
||||
|
@ -157,7 +242,7 @@ RSpec.describe Gitlab::Zentao::Client do
|
|||
let(:zentao_integration) { create(:zentao_integration, url: 'https://jihudemo.zentao.net') }
|
||||
|
||||
it 'joins url correctly' do
|
||||
expect(integration.send(:url, "products/1").to_s)
|
||||
expect(client.send(:url, "products/1").to_s)
|
||||
.to eq("https://jihudemo.zentao.net/api.php/v1/products/1")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,4 +81,24 @@ RSpec.describe Integrations::Zentao do
|
|||
expect(zentao_integration.help).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#client_url' do
|
||||
subject(:integration) { build(:zentao_integration, api_url: api_url, url: 'url').client_url }
|
||||
|
||||
context 'when api_url is set' do
|
||||
let(:api_url) { 'api_url' }
|
||||
|
||||
it 'returns the api_url' do
|
||||
is_expected.to eq(api_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when api_url is not set' do
|
||||
let(:api_url) { '' }
|
||||
|
||||
it 'returns the url' do
|
||||
is_expected.to eq('url')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -831,13 +831,21 @@ RSpec.describe Issue do
|
|||
end
|
||||
|
||||
describe '#to_branch_name exists ending with -index' do
|
||||
before do
|
||||
it 'returns #to_branch_name ending with max index + 1' do
|
||||
allow(repository).to receive(:branch_exists?).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with("#{subject.to_branch_name}-3").and_return(false)
|
||||
|
||||
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
|
||||
end
|
||||
|
||||
it 'returns #to_branch_name ending with max index + 1' do
|
||||
expect(subject.suggested_branch_name).to eq("#{subject.to_branch_name}-3")
|
||||
context 'when branch name still exists after 5 attempts' do
|
||||
it 'returns #to_branch_name ending with random characters' do
|
||||
allow(repository).to receive(:branch_exists?).with(subject.to_branch_name).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\d/).and_return(true)
|
||||
allow(repository).to receive(:branch_exists?).with(/#{subject.to_branch_name}-\h{8}/).and_return(false)
|
||||
|
||||
expect(subject.suggested_branch_name).to match(/#{subject.to_branch_name}-\h{8}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2625,7 +2625,7 @@ RSpec.describe Repository do
|
|||
end
|
||||
|
||||
shared_examples '#tree' do
|
||||
subject { repository.tree(sha, path, recursive: recursive, pagination_params: pagination_params) }
|
||||
subject { repository.tree(sha, path, recursive: recursive, skip_flat_paths: false, pagination_params: pagination_params) }
|
||||
|
||||
let(:sha) { :head }
|
||||
let(:path) { nil }
|
||||
|
|
|
@ -91,6 +91,45 @@ RSpec.describe Snippet do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'description validations' do
|
||||
let_it_be(:invalid_description) { 'a' * (described_class::DESCRIPTION_LENGTH_MAX * 2) }
|
||||
|
||||
context 'with existing snippets' do
|
||||
let(:snippet) { create(:personal_snippet, description: 'This is a valid content at the time of creation') }
|
||||
|
||||
it 'does not raise a validation error if the description is not changed' do
|
||||
snippet.title = 'new title'
|
||||
|
||||
expect(snippet).to be_valid
|
||||
end
|
||||
|
||||
it 'raises and error if the description is changed and the size is bigger than limit' do
|
||||
expect(snippet).to be_valid
|
||||
|
||||
snippet.description = invalid_description
|
||||
|
||||
expect(snippet).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'with new snippets' do
|
||||
it 'is valid when description is smaller than the limit' do
|
||||
snippet = build(:personal_snippet, description: 'Valid Desc')
|
||||
|
||||
expect(snippet).to be_valid
|
||||
end
|
||||
|
||||
it 'raises error when description is bigger than setting limit' do
|
||||
snippet = build(:personal_snippet, description: invalid_description)
|
||||
|
||||
aggregate_failures do
|
||||
expect(snippet).not_to be_valid
|
||||
expect(snippet.errors.messages_for(:description)).to include("is too long (2 MB). The maximum size is 1 MB.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
|
|
|
@ -12,29 +12,51 @@ RSpec.describe CommitPresenter do
|
|||
it { expect(presenter.web_path).to eq("/#{project.full_path}/-/commit/#{commit.sha}") }
|
||||
end
|
||||
|
||||
describe '#status_for' do
|
||||
subject { presenter.status_for('ref') }
|
||||
describe '#detailed_status_for' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
context 'when user can read_commit_status' do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(true)
|
||||
end
|
||||
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha, ref: 'ref') }
|
||||
|
||||
it 'returns commit status for ref' do
|
||||
pipeline = double
|
||||
status = double
|
||||
subject { presenter.detailed_status_for('ref')&.text }
|
||||
|
||||
expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline)
|
||||
expect(pipeline).to receive(:detailed_status).with(user).and_return(status)
|
||||
|
||||
expect(subject).to eq(status)
|
||||
end
|
||||
where(:read_commit_status, :read_pipeline, :expected_result) do
|
||||
true | true | 'passed'
|
||||
true | false | nil
|
||||
false | true | nil
|
||||
false | false | nil
|
||||
end
|
||||
|
||||
context 'when user can not read_commit_status' do
|
||||
it 'is nil' do
|
||||
is_expected.to eq(nil)
|
||||
with_them do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
|
||||
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status_for' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: commit.sha) }
|
||||
|
||||
subject { presenter.status_for }
|
||||
|
||||
where(:read_commit_status, :read_pipeline, :expected_result) do
|
||||
true | true | 'success'
|
||||
true | false | nil
|
||||
false | true | nil
|
||||
false | false | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(read_commit_status)
|
||||
allow(presenter).to receive(:can?).with(user, :read_pipeline, pipeline).and_return(read_pipeline)
|
||||
end
|
||||
|
||||
it { is_expected.to eq expected_result }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,11 +6,16 @@ RSpec.describe 'getting incident timeline events' do
|
|||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:private_project) { create(:project, :private) }
|
||||
let_it_be(:issue) { create(:issue, project: private_project) }
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
let_it_be(:updated_by_user) { create(:user) }
|
||||
let_it_be(:incident) { create(:incident, project: project) }
|
||||
let_it_be(:another_incident) { create(:incident, project: project) }
|
||||
let_it_be(:promoted_from_note) { create(:note, project: project, noteable: incident) }
|
||||
let_it_be(:issue_url) { project_issue_url(private_project, issue) }
|
||||
let_it_be(:issue_ref) { "#{private_project.full_path}##{issue.iid}" }
|
||||
let_it_be(:issue_link) { %Q(<a href="#{issue_url}">#{issue_url}</a>) }
|
||||
|
||||
let_it_be(:timeline_event) do
|
||||
create(
|
||||
|
@ -18,7 +23,8 @@ RSpec.describe 'getting incident timeline events' do
|
|||
incident: incident,
|
||||
project: project,
|
||||
updated_by_user: updated_by_user,
|
||||
promoted_from_note: promoted_from_note
|
||||
promoted_from_note: promoted_from_note,
|
||||
note: "Referencing #{issue.to_reference(full: true)} - Full URL #{issue_url}"
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -89,7 +95,7 @@ RSpec.describe 'getting incident timeline events' do
|
|||
'title' => incident.title
|
||||
},
|
||||
'note' => timeline_event.note,
|
||||
'noteHtml' => timeline_event.note_html,
|
||||
'noteHtml' => "<p>Referencing #{issue_ref} - Full URL #{issue_link}</p>",
|
||||
'promotedFromNote' => {
|
||||
'id' => promoted_from_note.to_global_id.to_s,
|
||||
'body' => promoted_from_note.note
|
||||
|
|
|
@ -763,6 +763,96 @@ RSpec.describe API::Search do
|
|||
it_behaves_like 'pagination', scope: :commits, search: 'merge'
|
||||
|
||||
it_behaves_like 'ping counters', scope: :commits
|
||||
|
||||
describe 'pipeline visibility' do
|
||||
shared_examples 'pipeline information visible' do
|
||||
it 'contains status and last_pipeline' do
|
||||
request
|
||||
|
||||
expect(json_response[0]['status']).to eq 'success'
|
||||
expect(json_response[0]['last_pipeline']).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'pipeline information not visible' do
|
||||
it 'does not contain status and last_pipeline' do
|
||||
request
|
||||
|
||||
expect(json_response[0]['status']).to be_nil
|
||||
expect(json_response[0]['last_pipeline']).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
let(:request) { get api(endpoint, user), params: { scope: 'commits', search: repo_project.commit.sha } }
|
||||
|
||||
before do
|
||||
create(:ci_pipeline, :success, project: repo_project, sha: repo_project.commit.sha)
|
||||
end
|
||||
|
||||
context 'with non public pipeline' do
|
||||
let_it_be(:repo_project) do
|
||||
create(:project, :public, :repository, public_builds: false, group: group)
|
||||
end
|
||||
|
||||
context 'user is project member with reporter role or above' do
|
||||
before do
|
||||
repo_project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is project member with guest role' do
|
||||
before do
|
||||
repo_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
|
||||
context 'user is not project member' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with public pipeline' do
|
||||
let_it_be(:repo_project) do
|
||||
create(:project, :public, :repository, public_builds: true, group: group)
|
||||
end
|
||||
|
||||
context 'user is project member with reporter role or above' do
|
||||
before do
|
||||
repo_project.add_reporter(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is project member with guest role' do
|
||||
before do
|
||||
repo_project.add_guest(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
end
|
||||
|
||||
context 'user is not project member' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it_behaves_like 'pipeline information visible'
|
||||
|
||||
context 'when CI/CD is set to only project members' do
|
||||
before do
|
||||
repo_project.project_feature.update!(builds_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
it_behaves_like 'pipeline information not visible'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for commits scope with project path as id' do
|
||||
|
|
|
@ -643,17 +643,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
end
|
||||
|
||||
context 'when username and password are provided' do
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the push attempt with personal access token error message' do
|
||||
it 'rejects the push attempt with generic error message' do
|
||||
upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -750,17 +750,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
|
||||
end
|
||||
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects pushes with personal access token error message' do
|
||||
it 'rejects pushes with generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -771,10 +771,10 @@ RSpec.describe 'Git HTTP requests' do
|
|||
.to receive(:login).and_return(nil)
|
||||
end
|
||||
|
||||
it 'does not display the personal access token error message' do
|
||||
it 'displays the generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1300,17 +1300,18 @@ RSpec.describe 'Git HTTP requests' do
|
|||
end
|
||||
|
||||
context 'when username and password are provided' do
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the push attempt with personal access token error message' do
|
||||
it 'rejects the push attempt with generic error message' do
|
||||
upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1381,17 +1382,17 @@ RSpec.describe 'Git HTTP requests' do
|
|||
allow_any_instance_of(ApplicationSetting).to receive(:password_authentication_enabled_for_git?) { false }
|
||||
end
|
||||
|
||||
it 'rejects pulls with personal access token error message' do
|
||||
it 'rejects pulls with generic error message' do
|
||||
download(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects pushes with personal access token error message' do
|
||||
it 'rejects pushes with generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1402,10 +1403,10 @@ RSpec.describe 'Git HTTP requests' do
|
|||
.to receive(:login).and_return(nil)
|
||||
end
|
||||
|
||||
it 'does not display the personal access token error message' do
|
||||
it 'returns a generic error message' do
|
||||
upload(path, user: 'foo', password: 'bar') do |response|
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
|
||||
expect(response.body).to eq('HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/topics/git/troubleshooting_git#error-on-git-fetch-http-basic-access-denied')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,22 @@ RSpec.describe JwtController do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples "with invalid credentials" do
|
||||
it "returns a generic error message" do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(json_response).to eq(
|
||||
{
|
||||
"errors" => [{
|
||||
"code" => "UNAUTHORIZED",
|
||||
"message" => "HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2FA enabled and you must use a personal access token instead of a password. See http://www.example.com/help/user/profile/account/two_factor_authentication#troubleshooting"
|
||||
}]
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticating against container registry' do
|
||||
context 'existing service' do
|
||||
subject! { get '/jwt/auth', params: parameters }
|
||||
|
@ -55,10 +71,7 @@ RSpec.describe JwtController do
|
|||
context 'with blocked user' do
|
||||
let(:user) { create(:user, :blocked) }
|
||||
|
||||
it 'rejects the request as unauthorized' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('HTTP Basic: Access denied')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -158,10 +171,7 @@ RSpec.describe JwtController do
|
|||
let(:user) { create(:user, :two_factor) }
|
||||
|
||||
context 'without personal token' do
|
||||
it 'rejects the authorization attempt' do
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
|
||||
context 'with personal token' do
|
||||
|
@ -185,14 +195,10 @@ RSpec.describe JwtController do
|
|||
|
||||
context 'using invalid login' do
|
||||
let(:headers) { { authorization: credentials('invalid', 'password') } }
|
||||
let(:subject) { get '/jwt/auth', params: parameters, headers: headers }
|
||||
|
||||
context 'when internal auth is enabled' do
|
||||
it 'rejects the authorization attempt' do
|
||||
get '/jwt/auth', params: parameters, headers: headers
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
|
||||
context 'when internal auth is disabled' do
|
||||
|
@ -200,12 +206,7 @@ RSpec.describe JwtController do
|
|||
stub_application_setting(password_authentication_enabled_for_git: false)
|
||||
end
|
||||
|
||||
it 'rejects the authorization attempt with personal access token message' do
|
||||
get '/jwt/auth', params: parameters, headers: headers
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
|
||||
end
|
||||
it_behaves_like 'with invalid credentials'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,6 +9,16 @@ RSpec.describe Glfm::Shared do
|
|||
end.new
|
||||
end
|
||||
|
||||
describe '#write_file' do
|
||||
it 'works' do
|
||||
filename = Dir::Tmpname.create('basename') do |path|
|
||||
instance.write_file(path, 'test')
|
||||
end
|
||||
|
||||
expect(File.read(filename)).to eq 'test'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#run_external_cmd' do
|
||||
it 'works' do
|
||||
expect(instance.run_external_cmd('echo "hello"')).to eq("hello\n")
|
||||
|
@ -24,6 +34,14 @@ RSpec.describe Glfm::Shared do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#dump_yaml_with_formatting' do
|
||||
it 'works' do
|
||||
hash = { a: 'b' }
|
||||
yaml = instance.dump_yaml_with_formatting(hash, literal_scalars: true)
|
||||
expect(yaml).to eq("---\na: |-\n b\n")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#output' do
|
||||
# NOTE: The #output method is normally always mocked, to prevent output while the specs are
|
||||
# running. However, in order to provide code coverage for the method, we have to invoke
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue