Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-10 21:30:44 +00:00
parent 3214c36592
commit 5aaced570d
64 changed files with 916 additions and 507 deletions

View File

@ -3020,7 +3020,6 @@ Style/InlineDisableAnnotation:
- 'spec/support/sidekiq_middleware.rb'
- 'spec/support_specs/ability_check_spec.rb'
- 'spec/support_specs/capybara_slow_finder_spec.rb'
- 'spec/support_specs/capybara_wait_for_all_requests_spec.rb'
- 'spec/support_specs/database/multiple_databases_helpers_spec.rb'
- 'spec/support_specs/helpers/stub_feature_flags_spec.rb'
- 'spec/support_specs/matchers/event_store_spec.rb'

View File

@ -26,7 +26,7 @@ export const i18n = {
'Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}.',
),
confirmationMessage: s__(
'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
'Branches|Please type the following to confirm: %{codeStart}delete%{codeEnd}.',
),
cancelButtonText: __('Cancel'),
actionsToggleText: __('More actions'),

View File

@ -1,12 +1,14 @@
<script>
import { GlSearchBoxByClick, GlSorting } from '@gitlab/ui';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
SORT_ASC,
SORT_DESC,
SORT_OPTION_CREATED,
SORT_OPTION_RELEASED,
SORT_OPTION_STAR_COUNT,
SORT_OPTION_POPULARITY,
} from '../../constants';
export default {
@ -14,6 +16,7 @@ export default {
GlSearchBoxByClick,
GlSorting,
},
mixins: [glFeatureFlagMixin()],
props: {
initialSearchTerm: {
default: '',
@ -29,6 +32,13 @@ export default {
};
},
computed: {
sortOptions() {
const options = [...this.$options.defaultSortOptions];
if (this.glFeatures?.ciCatalogPopularity) {
options.push({ value: SORT_OPTION_POPULARITY, text: __('Popularity') });
}
return options;
},
currentSortDirection() {
return this.isAscending ? SORT_ASC : SORT_DESC;
},
@ -36,9 +46,7 @@ export default {
return `${this.currentSortOption}_${this.currentSortDirection}`;
},
currentSortText() {
const currentSort = this.$options.sortOptions.find(
(sort) => sort.value === this.currentSortOption,
);
const currentSort = this.sortOptions.find((sort) => sort.value === this.currentSortOption);
return currentSort.text;
},
},
@ -61,7 +69,7 @@ export default {
this.currentSortOption = sortingItem;
},
},
sortOptions: [
defaultSortOptions: [
{ value: SORT_OPTION_RELEASED, text: __('Released at') },
{ value: SORT_OPTION_CREATED, text: __('Created at') },
{ value: SORT_OPTION_STAR_COUNT, text: __('Star count') },
@ -79,7 +87,7 @@ export default {
<gl-sorting
:is-ascending="isAscending"
:text="currentSortText"
:sort-options="$options.sortOptions"
:sort-options="sortOptions"
:sort-by="currentSortOption"
data-testid="catalog-sorting-option-button"
@sortByChange="setSelectedSortOption"

View File

@ -3,6 +3,7 @@ import {
GlAvatar,
GlBadge,
GlButton,
GlIcon,
GlLink,
GlSprintf,
GlTooltipDirective,
@ -14,6 +15,7 @@ import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { toNounSeriesText } from '~/lib/utils/grammar';
import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
import { VERIFICATION_LEVEL_UNVERIFIED } from '../../constants';
import CiVerificationBadge from '../shared/ci_verification_badge.vue';
@ -29,6 +31,7 @@ export default {
GlAvatar,
GlBadge,
GlButton,
GlIcon,
GlLink,
GlSprintf,
GlTruncate,
@ -37,6 +40,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
resource: {
type: Object,
@ -86,6 +90,9 @@ export default {
hasReleasedVersion() {
return Boolean(this.latestVersion?.createdAt);
},
isPopularityFeatureEnabled() {
return Boolean(this.glFeatures?.ciCatalogPopularity);
},
isVerified() {
return this.resource?.verificationLevel !== VERIFICATION_LEVEL_UNVERIFIED;
},
@ -107,6 +114,14 @@ export default {
starCountText() {
return n__('Star', 'Stars', this.starCount);
},
usageCount() {
return this.resource?.last30DayUsageCount || 0;
},
usageText() {
return s__(
'CiCatalog|The number of projects that used a component from this project in a pipeline, by using "include:component", in the last 30 days.',
);
},
webPath() {
return cleanLeadingSeparator(this.resource?.webPath);
},
@ -170,18 +185,31 @@ export default {
<gl-badge size="sm" class="gl-h-5 gl-align-self-center" variant="info">{{
name
}}</gl-badge>
<gl-button
v-gl-tooltip.top
data-testid="stats-favorites"
class="!gl-text-inherit"
icon="star-o"
:title="starCountText"
:href="starsHref"
size="small"
variant="link"
>
{{ starCount }}
</gl-button>
<div class="gl-display-flex gl-align-items-center gl-ml-3">
<div
v-if="isPopularityFeatureEnabled"
v-gl-tooltip.top
class="gl-display-flex gl-align-items-center gl-mr-3"
:title="usageText"
>
<gl-icon name="chart" :size="16" />
<span class="gl-ml-2" data-testid="stats-usage">
{{ usageCount }}
</span>
</div>
<gl-button
v-gl-tooltip.top
data-testid="stats-favorites"
class="!gl-text-inherit"
icon="star-o"
:title="starCountText"
:href="starsHref"
size="small"
variant="link"
>
{{ starCount }}
</gl-button>
</div>
</div>
</div>
<div

View File

@ -9,6 +9,7 @@ export const SCOPE = {
};
export const SORT_OPTION_CREATED = 'CREATED';
export const SORT_OPTION_POPULARITY = 'USAGE_COUNT';
export const SORT_OPTION_RELEASED = 'LATEST_RELEASED_AT';
export const SORT_OPTION_STAR_COUNT = 'STAR_COUNT';
export const SORT_ASC = 'ASC';

View File

@ -5,6 +5,7 @@ fragment CatalogResourceFields on CiCatalogResource {
icon
name
starCount
last30DayUsageCount
starrersPath
verificationLevel
versions(first: 1) {

View File

@ -18,7 +18,7 @@ import deletePipelineScheduleMutation from '../graphql/mutations/delete_pipeline
import playPipelineScheduleMutation from '../graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '../graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql';
import { ALL_SCOPE, SCHEDULES_PER_PAGE, DEFAULT_SORT_VALUE } from '../constants';
import { ALL_SCOPE, SCHEDULES_PER_PAGE } from '../constants';
import PipelineSchedulesTable from './table/pipeline_schedules_table.vue';
import TakeOwnershipModal from './take_ownership_modal.vue';
import DeletePipelineScheduleModal from './delete_pipeline_schedule_modal.vue';
@ -90,7 +90,6 @@ export default {
// we need to ensure we send null to the API when
// the scope is 'ALL'
status: this.scope === ALL_SCOPE ? null : this.scope,
sortValue: this.sortValue,
first: this.pagination.first,
last: this.pagination.last,
prevPageCursor: this.pagination.prevPageCursor,
@ -129,9 +128,6 @@ export default {
playSuccess: false,
errorMessage: '',
scheduleId: null,
sortValue: DEFAULT_SORT_VALUE,
sortBy: 'ID',
sortDesc: true,
showDeleteModal: false,
showTakeOwnershipModal: false,
count: 0,
@ -336,13 +332,6 @@ export default {
};
}
},
onUpdateSorting(sortValue, sortBy, sortDesc) {
this.sortValue = sortValue;
this.sortBy = sortBy;
this.sortDesc = sortDesc;
this.resetPagination();
},
},
};
</script>
@ -408,12 +397,9 @@ export default {
<pipeline-schedules-table
:schedules="schedules.list"
:current-user="schedules.currentUser"
:sort-by="sortBy"
:sort-desc="sortDesc"
@showTakeOwnershipModal="setTakeOwnershipModal"
@showDeleteModal="setDeleteModal"
@playPipelineSchedule="playPipelineSchedule"
@update-sorting="onUpdateSorting"
/>
<gl-pagination

View File

@ -1,7 +1,6 @@
<script>
import { GlTable } from '@gitlab/ui';
import { s__ } from '~/locale';
import { TH_DESCRIPTION_TEST_ID, TH_TARGET_TEST_ID, TH_NEXT_TEST_ID } from '../../constants';
import PipelineScheduleActions from './cells/pipeline_schedule_actions.vue';
import PipelineScheduleLastPipeline from './cells/pipeline_schedule_last_pipeline.vue';
import PipelineScheduleNextRun from './cells/pipeline_schedule_next_run.vue';
@ -15,21 +14,15 @@ export default {
fields: [
{
key: 'description',
actualSortKey: 'DESCRIPTION',
label: s__('PipelineSchedules|Description'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-8/20',
sortable: true,
thAttr: TH_DESCRIPTION_TEST_ID,
},
{
key: 'target',
actualSortKey: 'REF',
sortable: true,
label: s__('PipelineSchedules|Target'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-2/20',
thAttr: TH_TARGET_TEST_ID,
},
{
key: 'pipeline',
@ -39,12 +32,9 @@ export default {
},
{
key: 'next',
actualSortKey: 'NEXT_RUN_AT',
label: s__('PipelineSchedules|Next Run'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-3/20',
sortable: true,
thAttr: TH_NEXT_TEST_ID,
},
{
key: 'owner',
@ -76,24 +66,6 @@ export default {
type: Object,
required: true,
},
sortBy: {
type: String,
required: true,
},
sortDesc: {
type: Boolean,
required: true,
},
},
methods: {
fetchSortedData({ sortBy, sortDesc }) {
const field = this.$options.fields.find(({ key }) => key === sortBy);
const sortingDirection = sortDesc ? 'DESC' : 'ASC';
if (!field?.actualSortKey) return;
this.$emit('update-sorting', `${field.actualSortKey}_${sortingDirection}`, sortBy, sortDesc);
},
},
};
</script>
@ -104,11 +76,8 @@ export default {
:items="schedules"
:tbody-tr-attr="{ 'data-testid': 'pipeline-schedule-table-row' }"
:empty-text="$options.i18n.emptyText"
:sort-by="sortBy"
:sort-desc="sortDesc"
show-empty
stacked="md"
@sort-changed="fetchSortedData"
>
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />

View File

@ -2,8 +2,3 @@ export const VARIABLE_TYPE = 'ENV_VAR';
export const FILE_TYPE = 'FILE';
export const ALL_SCOPE = 'ALL';
export const SCHEDULES_PER_PAGE = 50;
export const DEFAULT_SORT_VALUE = 'ID_DESC';
export const TH_DESCRIPTION_TEST_ID = { 'data-testid': 'pipeline-schedules-description-sort' };
export const TH_TARGET_TEST_ID = { 'data-testid': 'pipeline-schedules-target-sort' };
export const TH_NEXT_TEST_ID = { 'data-testid': 'pipeline-schedules-next-sort' };

View File

@ -4,7 +4,6 @@ query getPipelineSchedulesQuery(
$projectPath: ID!
$status: PipelineScheduleStatus
$ids: [ID!] = null
$sortValue: PipelineScheduleSort
$first: Int
$last: Int
$prevPageCursor: String = ""
@ -22,7 +21,6 @@ query getPipelineSchedulesQuery(
pipelineSchedules(
status: $status
ids: $ids
sort: $sortValue
first: $first
last: $last
after: $nextPageCursor

View File

@ -22,6 +22,11 @@ export default {
type: String,
required: true,
},
containerName: {
type: String,
required: false,
default: '',
},
namespace: {
type: String,
required: true,
@ -49,6 +54,7 @@ export default {
configuration: this.k8sAccessConfiguration,
namespace: this.namespace,
podName: this.podName,
containerName: this.containerName,
};
},
skip() {
@ -98,9 +104,17 @@ export default {
isLoading() {
return this.$apollo.queries.k8sLogs.loading || this.$apollo.queries.environment.loading;
},
emptyStateTitle() {
return this.containerName
? this.$options.i18n.emptyStateTitleForContainer
: this.$options.i18n.emptyStateTitleForPod;
},
},
i18n: {
emptyStateTitle: s__('KubernetesLogs|No logs available for pod %{podName}'),
emptyStateTitleForPod: s__('KubernetesLogs|No logs available for pod %{podName}'),
emptyStateTitleForContainer: s__(
'KubernetesLogs|No logs available for container %{containerName} of pod %{podName}',
),
},
EmptyStateSvg,
};
@ -117,8 +131,9 @@ export default {
<gl-empty-state v-else :svg-path="$options.EmptyStateSvg">
<template #title>
<h3>
<gl-sprintf :message="$options.i18n.emptyStateTitle"
><template #podName>{{ podName }}</template>
<gl-sprintf :message="emptyStateTitle"
><template #podName>{{ podName }}</template
><template #containerName>{{ containerName }}</template>
</gl-sprintf>
</h3>
</template>

View File

@ -1,5 +1,15 @@
#import "~/kubernetes_dashboard/graphql/queries/workload_item.fragment.graphql"
query getK8sLogs($configuration: LocalConfiguration, $namespace: String, $podName: String) {
k8sLogs(configuration: $configuration, namespace: $namespace, podName: $podName) @client
query getK8sLogs(
$configuration: LocalConfiguration
$namespace: String
$podName: String
$containerName: String
) {
k8sLogs(
configuration: $configuration
namespace: $namespace
podName: $podName
containerName: $containerName
) @client
}

View File

@ -47,16 +47,19 @@ class LogsCacheWrapper {
}
}
export const k8sLogs = (_, { configuration, namespace, podName }, { client }) => {
export const k8sLogs = (_, { configuration, namespace, podName, containerName }, { client }) => {
const config = new Configuration(configuration);
const watchApi = new WatchApi(config);
const watchPath = buildWatchPath({ resource: podName, namespace });
const variables = { configuration, namespace, podName };
const variables = { configuration, namespace, podName, containerName };
const cacheWrapper = new LogsCacheWrapper(client, variables);
const watchQuery = { follow: true };
if (containerName) watchQuery.container = containerName;
watchApi
.subscribeToStream(watchPath, { follow: true })
.subscribeToStream(watchPath, watchQuery)
.then((watcher) => {
watcher.on(EVENT_PLAIN_TEXT, (data) => {
const logsData = cacheWrapper.readLogsData();

View File

@ -80,6 +80,7 @@ export const initPage = async () => {
},
component: () => import('./environment_details/components/kubernetes/kubernetes_logs.vue'),
props: (route) => ({
containerName: route.query.container,
podName: route.params.podName,
namespace: route.params.namespace,
environmentName: dataSet.name,

View File

@ -0,0 +1,52 @@
<script>
import { GlAlert, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
name: 'WebIdeError',
components: {
GlAlert,
GlButton,
GlSprintf,
GlLink,
},
props: {
signOutPath: {
type: String,
required: true,
},
},
methods: {
reload: () => window.location.reload(),
},
i18n: {
title: __('Failed to load the Web IDE'),
message: __(
'For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, %{reportIssueStart}report a problem%{reportIssueEnd}.',
),
primaryButtonText: __('Reload'),
secondaryButtonText: __('Sign out'),
},
REPORT_ISSUE_URL: helpPagePath('user/project/web_ide/index', { anchor: '#report-a-problem' }),
};
</script>
<template>
<div class="gl-max-w-80 m-auto gl-pt-6">
<gl-alert variant="danger" :dismissible="false" :title="$options.i18n.title">
<gl-sprintf :message="$options.i18n.message">
<template #reportIssue="{ content }">
<gl-link :href="$options.REPORT_ISSUE_URL" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<template #actions>
<gl-button variant="confirm" category="primary" @click="reload">{{
$options.i18n.primaryButtonText
}}</gl-button>
<gl-button category="secondary" class="gl-ml-3" data-method="post" :href="signOutPath">
{{ $options.i18n.secondaryButtonText }}
</gl-button>
</template>
</gl-alert>
</div>
</template>

View File

@ -13,6 +13,7 @@ import {
handleTracking,
} from './lib/gitlab_web_ide';
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
import { renderWebIdeError } from './render_web_ide_error';
const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
const remotePath = cleanLeadingSeparator(remotePathArg);
@ -55,6 +56,7 @@ export const initGitlabWebIDE = async (el) => {
editorFont: editorFontJSON,
codeSuggestionsEnabled,
extensionsGallerySettings: extensionsGallerySettingsJSON,
signOutPath,
} = el.dataset;
const rootEl = setupRootElement(el);
@ -75,54 +77,58 @@ export const initGitlabWebIDE = async (el) => {
'X-Requested-With': 'XMLHttpRequest',
};
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
start(rootEl, {
...getBaseConfig(),
nonce,
httpHeaders,
auth: oauthConfig,
projectPath,
ref,
filePath,
mrId,
mrTargetProject: getMRTargetProject(),
forkInfo,
username: gon.current_username,
links: {
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
userPreferences: el.dataset.userPreferencesPath,
signIn: el.dataset.signInPath,
},
featureFlags: {
settingsSync: true,
crossOriginExtensionHost: getCrossOriginExtensionHostFlagValue(extensionsGallerySettings),
},
editorFont,
extensionsGallerySettings,
codeSuggestionsEnabled,
handleTracking,
// See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
telemetryEnabled: Tracking.enabled(),
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
const confirmed = await confirmAction(
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
{
primaryBtnText: __('Start remote connection'),
cancelBtnText: __('Continue editing'),
},
);
try {
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
await start(rootEl, {
...getBaseConfig(),
nonce,
httpHeaders,
auth: oauthConfig,
projectPath,
ref,
filePath,
mrId,
mrTargetProject: getMRTargetProject(),
forkInfo,
username: gon.current_username,
links: {
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
userPreferences: el.dataset.userPreferencesPath,
signIn: el.dataset.signInPath,
},
featureFlags: {
settingsSync: true,
crossOriginExtensionHost: getCrossOriginExtensionHostFlagValue(extensionsGallerySettings),
},
editorFont,
extensionsGallerySettings,
codeSuggestionsEnabled,
handleTracking,
// See https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L86
telemetryEnabled: Tracking.enabled(),
async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
const confirmed = await confirmAction(
__('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
{
primaryBtnText: __('Start remote connection'),
cancelBtnText: __('Continue editing'),
},
);
if (!confirmed) {
return;
}
if (!confirmed) {
return;
}
createAndSubmitForm({
url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
data: {
connection_token: connectionToken,
return_url: window.location.href,
},
});
},
});
createAndSubmitForm({
url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
data: {
connection_token: connectionToken,
return_url: window.location.href,
},
});
},
});
} catch (error) {
renderWebIdeError({ error, signOutPath });
}
};

View File

@ -0,0 +1,23 @@
import Vue from 'vue';
import { logError } from '~/lib/logger';
import WebIdeError from '~/ide/components/web_ide_error.vue';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
export function renderWebIdeError({ error, signOutPath }) {
// eslint-disable-next-line @gitlab/require-i18n-strings
logError('Failed to load Web IDE', error);
Sentry.captureException(error);
const alertContainer = document.querySelector('.flash-container');
if (!alertContainer) return null;
const el = document.createElement('div');
alertContainer.appendChild(el);
return new Vue({
el,
render(createElement) {
return createElement(WebIdeError, { props: { signOutPath } });
},
});
}

View File

@ -107,7 +107,7 @@ export default {
</script>
<template>
<div>
<div class="gl-border-t">
<refs-list
v-if="hasBranches"
:has-containing-refs="hasContainingBranches"

View File

@ -78,7 +78,7 @@ export default {
</script>
<template>
<div class="ref-list gl-p-5 gl-border-b-solid gl-border-b-1">
<div class="well-segment">
<gl-icon :name="refIcon" :size="14" class="gl-ml-2 gl-mr-3" />
<span data-testid="title" class="gl-mr-2">{{ namespace }}</span>
<gl-badge

View File

@ -38,6 +38,7 @@ export default {
href: chunk,
class: 'gl-reset-color! gl-text-decoration-underline',
rel: 'nofollow noopener noreferrer', // eslint-disable-line @gitlab/require-i18n-strings
target: '_blank',
},
},
chunk,

View File

@ -12,6 +12,6 @@
* an opening or closing square bracket
* [[\]]
*/
export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])(?<![[\]])/g;
export const linkRegex = /(https?:\/\/[^"'<>()\\^`{|}\s]+[^"'<>()\\^`{|}\s.,:;!?])(?<![[\]])/g;
export default { linkRegex };

View File

@ -7,6 +7,9 @@ module Explore
feature_category :pipeline_composition
before_action :check_resource_access, only: :show
track_internal_event :index, name: 'unique_users_visiting_ci_catalog', conditions: :current_user
before_action do
push_frontend_feature_flag(:ci_catalog_popularity)
end
def show; end

View File

@ -7,6 +7,8 @@ module Groups
feature_category :package_registry
urgency :low
before_action :set_feature_flag_packages_protected_packages, only: :show
# The show action renders index to allow frontend routing to work on page refresh
def show
render :index
@ -17,5 +19,9 @@ module Groups
def verify_packages_enabled!
render_404 unless group.packages_feature_enabled?
end
def set_feature_flag_packages_protected_packages
push_frontend_feature_flag(:packages_protected_packages, group)
end
end
end

View File

@ -7,6 +7,7 @@ module IdeHelper
'use-new-web-ide' => use_new_web_ide?.to_s,
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index', anchor: 'vscode-reimplementation'),
'sign-in-path' => new_session_path(current_user),
'sign-out-path' => destroy_user_session_path,
'user-preferences-path' => profile_preferences_path
}.merge(use_new_web_ide? ? new_ide_data(project: project) : legacy_ide_data(project: project))

View File

@ -583,23 +583,15 @@ class Commit
end
def branches_containing(limit: 0, exclude_tipped: false)
# WARNING: This argument can be confusing, if there is a limit.
# for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
# then the method will only 3 refs, even though there is more.
excluded = exclude_tipped ? tipping_branches : []
refs = repository.branch_names_contains(id, limit: limit) || []
refs - excluded
repository.branch_names_contains(id, limit: limit, exclude_refs: excluded) || []
end
def tags_containing(limit: 0, exclude_tipped: false)
# WARNING: This argument can be confusing, if there is a limit.
# for example set the limit to 5 and in the 5 out a total of 25 refs there is 2 tipped refs,
# then the method will only 3 refs, even though there is more.
excluded = exclude_tipped ? tipping_tags : []
refs = repository.tag_names_contains(id, limit: limit) || []
refs - excluded
repository.tag_names_contains(id, limit: limit, exclude_refs: excluded) || []
end
private

View File

@ -51,6 +51,7 @@ module HasUserType
scope :without_ghosts, -> { where(user_type: USER_TYPES.keys - ['ghost']) }
scope :without_project_bot, -> { where(user_type: USER_TYPES.keys - ['project_bot']) }
scope :human_or_service_user, -> { where(user_type: %i[human service_user]) }
scope :resource_access_token_bot, -> { where(user_type: 'project_bot') }
validates :user_type, presence: true
end

View File

@ -787,12 +787,16 @@ class Repository
Commit.order_by(collection: commits, order_by: order_by, sort: sort)
end
def branch_names_contains(sha, limit: 0)
raw_repository.branch_names_contains_sha(sha, limit: limit)
def branch_names_contains(sha, limit: 0, exclude_refs: [])
refs = raw_repository.branch_names_contains_sha(sha, limit: adjust_containing_limit(limit: limit, exclude_refs: exclude_refs))
adjust_containing_refs(limit: limit, refs: refs - exclude_refs)
end
def tag_names_contains(sha, limit: 0)
raw_repository.tag_names_contains_sha(sha, limit: limit)
def tag_names_contains(sha, limit: 0, exclude_refs: [])
refs = raw_repository.tag_names_contains_sha(sha, limit: adjust_containing_limit(limit: limit, exclude_refs: exclude_refs))
adjust_containing_refs(limit: limit, refs: refs - exclude_refs)
end
def local_branches
@ -1306,6 +1310,22 @@ class Repository
private
# Increase the limit by number of excluded refs
# to prevent a situation when we return less refs than requested
def adjust_containing_limit(limit:, exclude_refs:)
return limit if limit == 0
limit + exclude_refs.size
end
# Limit number of returned refs
# in case the result has more refs than requested
def adjust_containing_refs(limit:, refs:)
return refs if limit == 0
refs.take(limit)
end
def ancestor_cache_key(ancestor_id, descendant_id)
"ancestor:#{ancestor_id}:#{descendant_id}"
end

View File

@ -607,13 +607,6 @@ class User < MainClusterwide::ApplicationRecord
scope :with_emails, -> { preload(:emails) }
scope :with_dashboard, ->(dashboard) { where(dashboard: dashboard) }
scope :with_public_profile, -> { where(private_profile: false) }
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
where('EXISTS (?)', ::PersonalAccessToken
.where('personal_access_tokens.user_id = users.id')
.without_impersonation
.expiring_and_not_notified(at).select(1)
)
end
scope :with_personal_access_tokens_expired_today, -> do
where('EXISTS (?)', ::PersonalAccessToken
.select(1)

View File

@ -18,4 +18,4 @@
- data = ide_data(project: @project, fork_info: @fork_info, params: params)
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the GitLab IDE') }
= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Loading the Web IDE') }

View File

@ -19,7 +19,7 @@ module PersonalAccessTokens
def perform(*args)
process_user_tokens
process_project_access_tokens
process_project_bot_tokens
end
private
@ -59,12 +59,14 @@ module PersonalAccessTokens
# rubocop: enable CodeReuse/ActiveRecord
end
def process_project_access_tokens
def process_project_bot_tokens
# rubocop: disable CodeReuse/ActiveRecord -- We need to specify batch size to avoid timing out of worker
notifications_delivered = 0
project_bot_ids_without_resource = []
project_bot_ids_with_failed_delivery = []
loop do
tokens = PersonalAccessToken
.without_impersonation
.where.not(user_id: project_bot_ids_without_resource | project_bot_ids_with_failed_delivery)
.expiring_and_not_notified_without_impersonation
.project_access_token
.select(:id, :user_id)
@ -75,37 +77,68 @@ module PersonalAccessTokens
bot_users = User.id_in(tokens.pluck(:user_id).uniq).with_personal_access_tokens_and_resources
bot_users.each do |user|
with_context(user: user) do
expiring_user_token = user.personal_access_tokens.first # bot user should not have more than 1 token
bot_users.each do |project_bot|
if project_bot.resource_bot_resource.nil?
project_bot_ids_without_resource << project_bot.id
execute_web_hooks(user, expiring_user_token)
deliver_bot_notifications(user, expiring_user_token.name)
next
end
begin
with_context(user: project_bot) do
# project bot does not have more than 1 token
expiring_user_token = project_bot.personal_access_tokens.first
execute_web_hooks(project_bot, expiring_user_token)
deliver_bot_notifications(project_bot, expiring_user_token.name)
end
rescue StandardError => e
project_bot_ids_with_failed_delivery << project_bot.id
log_exception(e, project_bot)
end
end
tokens.update_all(expire_notification_delivered: true)
notifications_delivered += tokens.count
tokens_with_delivered_notifications =
tokens
.where.not(user_id: project_bot_ids_without_resource | project_bot_ids_with_failed_delivery)
tokens_with_delivered_notifications.update_all(expire_notification_delivered: true)
notifications_delivered += tokens_with_delivered_notifications.count
end
log_extra_metadata_on_done(:total_notification_delivered_for_bot_personal_access_tokens, notifications_delivered)
log_extra_metadata_on_done(
:total_notification_delivered_for_resource_access_tokens, notifications_delivered)
log_extra_metadata_on_done(
:total_resource_bot_without_membership, project_bot_ids_without_resource.count)
log_extra_metadata_on_done(
:total_failed_notifications_for_resource_bots, project_bot_ids_with_failed_delivery.count)
# rubocop: enable CodeReuse/ActiveRecord
end
def deliver_bot_notifications(bot_user, token_name)
notification_service.bot_resource_access_token_about_to_expire(bot_user, token_name)
Gitlab::AppLogger.info(
message: "Notifying Bot User resource owners about expiring tokens",
class: self.class,
user_id: bot_user.id
)
end
def deliver_user_notifications(user, token_names)
notification_service.access_token_about_to_expire(user, token_names)
log_info("Notifying User about expiring tokens", user)
end
def log_info(message_text, user)
Gitlab::AppLogger.info(
message: "Notifying User about expiring tokens",
message: message_text,
class: self.class,
user_id: user.id
)
end
def log_exception(ex, user)
Gitlab::AppLogger.error(
message: 'Failed to send notification about expiring resource access tokens',
'exception.message': ex.message,
'exception.class': ex.class.name,
class: self.class,
user_id: user.id
)
@ -114,6 +147,7 @@ module PersonalAccessTokens
def execute_web_hooks(bot_user, token)
resource = bot_user.resource_bot_resource
return unless resource
return if resource.is_a?(Project) && !resource.has_active_hooks?(:resource_access_token_hooks)
hook_data = Gitlab::DataBuilder::ResourceAccessToken.build(token, :expiring, resource)

View File

@ -0,0 +1,9 @@
---
name: ci_job_artifacts_use_primary_to_authenticate
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466138
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/155684
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/466145
milestone: '17.1'
group: group::pipeline execution
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: ci_catalog_popularity
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/434333
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/464593
milestone: '17.1'
type: wip
group: group::pipeline authoring
default_enabled: false

View File

@ -1051,6 +1051,21 @@ This file is located at:
- `/var/log/gitlab/gitlab-rails/epic_work_item_sync.log` on Linux package installations.
- `/home/git/gitlab/log/epic_work_item_sync.log` on self-compiled installations.
## `secret_detection.log`
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab Dedicated
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137812) in GitLab 16.7.
The `secret_detection.log` file logs information related to [Secret Push Protection](../../user/application_security/secret_detection/secret_push_protection/index.md) feature.
This file is located at:
- `/var/log/gitlab/gitlab-rails/secret_detection.log` on Linux package installations.
- `/home/git/gitlab/log/secret_detection.log` on self-compiled installations.
## Registry logs
For Linux package installations, container registry logs are in `/var/log/gitlab/registry/current`.

View File

@ -250,7 +250,7 @@ When reviewing code changes, you can hide inline comments:
1. Below the title, select **Changes**.
1. Scroll to the file that contains the comments you want to hide.
1. Scroll to the line the comment is attached to, and select **Collapse** (**{collapse}**):
![collapse a comment](img/collapse-comment_v14_8.png)
![collapse a comment](img/collapse-comment_v17_1.png)
To expand inline comments and show them again:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -280,3 +280,15 @@ As a workaround:
or modify the `"editor.fontFamily"` setting.
For more information, see [VS Code issue 80170](https://github.com/microsoft/vscode/issues/80170).
### Report a problem
To report a problem, [create a new issue](https://gitlab.com/gitlab-org/gitlab-web-ide/-/issues/new)
with the following information:
- The error message
- The full error details
- How often the problem occurs
- Steps to reproduce the problem
If you're on a paid tier, you can also [contact Support](https://about.gitlab.com/support/#contact-support) for help.

View File

@ -100,8 +100,13 @@ module API
job
end
def authenticate_job_via_dependent_job!
authenticate!
def authenticate_job_via_dependent_job!(use_primary_to_authenticate: false)
if use_primary_to_authenticate
::Gitlab::Database::LoadBalancing::Session.current.use_primary { authenticate! }
else
authenticate!
end
forbidden! unless current_job
forbidden! unless can?(current_user, :read_build, current_job)
end

View File

@ -385,7 +385,8 @@ module API
end
route_setting :authentication, job_token_allowed: true
get '/:id/artifacts', feature_category: :build_artifacts do
authenticate_job_via_dependent_job!
use_primary = Feature.enabled?(:ci_job_artifacts_use_primary_to_authenticate, current_job&.project)
authenticate_job_via_dependent_job!(use_primary_to_authenticate: use_primary)
audit_download(current_job, current_job.artifacts_file&.filename) if current_job.artifacts_file
present_artifacts_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download])

View File

@ -9368,7 +9368,7 @@ msgstr ""
msgid "Branches|Please type the following to confirm:"
msgstr ""
msgid "Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}."
msgid "Branches|Please type the following to confirm: %{codeStart}delete%{codeEnd}."
msgstr ""
msgid "Branches|See all branch-related settings together with branch rules"
@ -11129,6 +11129,9 @@ msgstr ""
msgid "CiCatalog|Set component project as a CI/CD Catalog project. %{linkStart}What is the CI/CD Catalog?%{linkEnd}"
msgstr ""
msgid "CiCatalog|The number of projects that used a component from this project in a pipeline, by using \"include:component\", in the last 30 days."
msgstr ""
msgid "CiCatalog|The project and any released versions will be removed from the CI/CD Catalog. If you re-enable this toggle, the project's existing releases are not re-added to the catalog. You must %{linkStart}create a new release%{linkEnd}."
msgstr ""
@ -21838,6 +21841,9 @@ msgstr ""
msgid "Failed to load stacktrace."
msgstr ""
msgid "Failed to load the Web IDE"
msgstr ""
msgid "Failed to make repository read-only: %{reason}"
msgstr ""
@ -22547,6 +22553,9 @@ msgstr ""
msgid "For more information, see the File Hooks documentation."
msgstr ""
msgid "For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, %{reportIssueStart}report a problem%{reportIssueEnd}."
msgstr ""
msgid "For the GitLab Team to keep your subscription data up to date, this is a reminder to report your license usage on a monthly basis, or at the cadence set in your agreement with GitLab. This allows us to simplify the billing process for overages and renewals. To report your usage data, export your license usage file and email it to %{renewal_service_email}. If you need an updated license, GitLab will send the license to the email address registered in the %{customers_dot}, and you can upload this license to your instance."
msgstr ""
@ -30161,6 +30170,9 @@ msgstr ""
msgid "KubernetesDashboard|You can select an agent from a project's environment page."
msgstr ""
msgid "KubernetesLogs|No logs available for container %{containerName} of pod %{podName}"
msgstr ""
msgid "KubernetesLogs|No logs available for pod %{podName}"
msgstr ""
@ -31063,7 +31075,7 @@ msgstr ""
msgid "Loading snippet"
msgstr ""
msgid "Loading the GitLab IDE"
msgid "Loading the Web IDE"
msgstr ""
msgid "Loading..."
@ -43417,6 +43429,9 @@ msgstr ""
msgid "Release|You can edit the content later by editing the release. %{linkStart}How do I edit a release?%{linkEnd}"
msgstr ""
msgid "Reload"
msgstr ""
msgid "Reload page"
msgstr ""
@ -45218,6 +45233,9 @@ msgstr ""
msgid "Runners|Most recent failures"
msgstr ""
msgid "Runners|Most used group runners"
msgstr ""
msgid "Runners|Most used instance runners"
msgstr ""
@ -45685,6 +45703,9 @@ msgstr ""
msgid "Runners|Token expiry"
msgstr ""
msgid "Runners|Top projects consuming group runners"
msgstr ""
msgid "Runners|Top projects consuming runners"
msgstr ""

View File

@ -19,7 +19,7 @@ gem 'faker', '~> 3.4', '>= 3.4.1'
gem 'knapsack', '~> 4.0'
gem 'parallel_tests', '~> 4.7', '>= 4.7.1'
gem 'rotp', '~> 6.3.0'
gem 'parallel', '~> 1.24'
gem 'parallel', '~> 1.25', '>= 1.25.1'
gem 'rainbow', '~> 3.1.1'
gem 'rspec-parameterized', '~> 1.0.2'
gem 'octokit', '~> 8.1.0'

View File

@ -237,7 +237,7 @@ GEM
faraday (>= 1, < 3)
sawyer (~> 0.9)
os (1.1.4)
parallel (1.24.0)
parallel (1.25.1)
parallel_tests (4.7.1)
parallel
parser (3.3.0.5)
@ -399,7 +399,7 @@ DEPENDENCIES
knapsack (~> 4.0)
nokogiri (~> 1.16, >= 1.16.5)
octokit (~> 8.1.0)
parallel (~> 1.24)
parallel (~> 1.25, >= 1.25.1)
parallel_tests (~> 4.7, >= 4.7.1)
pry-byebug (~> 3.10.1)
rainbow (~> 3.1.1)

View File

@ -37,56 +37,17 @@ module QA
context "when tls is disabled" do
where do
{
'using docker:20.10.23 and a personal access token' => {
docker_client_version: 'docker:20.10.23',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412807'
},
'using docker:20.10.23 and a project deploy token' => {
docker_client_version: 'docker:20.10.23',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412808'
},
'using docker:20.10.23 and a ci job token' => {
docker_client_version: 'docker:20.10.23',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412809'
},
'using docker:23.0.6 and a personal access token' => {
docker_client_version: 'docker:23.0.6',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412810'
},
'using docker:23.0.6 and a project deploy token' => {
docker_client_version: 'docker:23.0.6',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412813'
},
'using docker:23.0.6 and a ci job token' => {
docker_client_version: 'docker:23.0.6',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412814'
},
'using docker:24.0.1 and a personal access token' => {
docker_client_version: 'docker:24.0.1',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412817'
},
'using docker:24.0.1 and a project deploy token' => {
docker_client_version: 'docker:24.0.1',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412818'
},
'using docker:24.0.1 and a ci job token' => {
docker_client_version: 'docker:24.0.1',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/412819'
@ -125,10 +86,10 @@ module QA
file_path: '.gitlab-ci.yml',
content: <<~YAML
build:
image: "#{docker_client_version}"
image: "docker:24.0.1"
stage: build
services:
- name: "#{docker_client_version}-dind"
- name: "docker:24.0.1-dind"
command: ["--insecure-registry=gitlab.test:5050"]
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
@ -178,10 +139,10 @@ module QA
file_path: '.gitlab-ci.yml',
content: <<~YAML
build:
image: docker:23.0.6
image: "docker:24.0.1"
stage: build
services:
- name: docker:23.0.6-dind
- name: "docker:24.0.1-dind"
command:
- /bin/sh
- -c

View File

@ -12,8 +12,6 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
let(:labels_value) { find_by_testid('value-wrapper') }
it 'updates the assignee in real-time' do
Capybara::Session.new(:other_session)
using_session :other_session do
visit project_issue_path(project, issue)
expect(page.find('.assignee')).to have_content 'None'
@ -34,8 +32,6 @@ RSpec.describe 'Issues > Real-time sidebar', :js, :with_license, feature_categor
end
it 'updates the label in real-time' do
Capybara::Session.new(:other_session)
using_session :other_session do
visit project_issue_path(project, issue)
wait_for_requests

View File

@ -15,10 +15,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
it 'user sees their active sessions' do
travel_to(Time.zone.parse('2018-03-12 09:06')) do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
Capybara::Session.new(:session3)
# note: headers can only be set on the non-js (aka. rack-test) driver
using_session :session1 do
Capybara.page.driver.header(
@ -83,8 +79,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
end
it 'admin sees if the session is with admin mode', :enable_admin_mode do
Capybara::Session.new(:admin_session)
using_session :admin_session do
gitlab_sign_in(admin)
visit user_settings_active_sessions_path
@ -93,8 +87,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
end
it 'does not display admin mode text in case its not' do
Capybara::Session.new(:admin_session)
using_session :admin_session do
gitlab_sign_in(admin)
visit user_settings_active_sessions_path
@ -103,9 +95,6 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
end
it 'user can revoke a session', :js do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
# set an additional session in another browser
using_session :session2 do
gitlab_sign_in(user)

View File

@ -73,7 +73,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
.
</p>
<p>
Plese type the following to confirm:
Please type the following to confirm:
<code>
delete
</code>

View File

@ -16,20 +16,22 @@ describe('CatalogSearch', () => {
const findSorting = () => wrapper.findComponent(GlSorting);
const findAllSortingItems = () => findSorting().props('sortOptions');
const createComponent = () => {
wrapper = shallowMountExtended(CatalogSearch, {});
const createComponent = ({ withCatalogPopularity = false } = {}) => {
wrapper = shallowMountExtended(CatalogSearch, {
provide: { glFeatures: { ciCatalogPopularity: withCatalogPopularity } },
});
};
beforeEach(() => {
createComponent();
});
describe('default UI', () => {
beforeEach(() => {
createComponent();
});
it('renders the search bar', () => {
expect(findSearchBar().exists()).toBe(true);
});
it('sets sorting options', () => {
it('adds sorting options', () => {
const sortOptionsProp = findAllSortingItems();
expect(sortOptionsProp).toHaveLength(3);
expect(sortOptionsProp[0].text).toBe('Released at');
@ -40,7 +42,23 @@ describe('CatalogSearch', () => {
});
});
describe('with `ci_catalog_popularity` ff turned on', () => {
beforeEach(() => {
createComponent({ withCatalogPopularity: true });
});
it('adds the popularity option', () => {
const sortOptionsProp = findAllSortingItems();
expect(sortOptionsProp).toHaveLength(4);
expect(sortOptionsProp[3].text).toBe('Popularity');
});
});
describe('search', () => {
beforeEach(() => {
createComponent();
});
it('passes down the search value to the search component', async () => {
const newSearchTerm = 'cat';
@ -85,6 +103,10 @@ describe('CatalogSearch', () => {
});
describe('sort', () => {
beforeEach(() => {
createComponent();
});
describe('when changing sort order', () => {
it('changes the `isAscending` prop to the sorting component', async () => {
expect(findSorting().props().isAscending).toBe(false);

View File

@ -29,13 +29,18 @@ describe('CiResourcesListItem', () => {
resource,
};
const createComponent = ({ props = {} } = {}) => {
const createComponent = ({ props = {}, withCatalogPopularity = false } = {}) => {
wrapper = shallowMountExtended(CiResourcesListItem, {
router,
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
ciCatalogPopularity: withCatalogPopularity,
},
},
stubs: {
GlSprintf,
GlTruncate,
@ -51,6 +56,7 @@ describe('CiResourcesListItem', () => {
const findVerificationBadge = () => wrapper.findComponent(CiVerificationBadge);
const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
const findFavorites = () => wrapper.findByTestId('stats-favorites');
const findUsage = () => wrapper.findByTestId('stats-usage');
const findMarkdown = () => wrapper.findComponent(Markdown);
beforeEach(() => {
@ -286,8 +292,29 @@ describe('CiResourcesListItem', () => {
});
});
describe('with FF `ci_catalog_popularity` turned on', () => {
beforeEach(() => {
createComponent({ withCatalogPopularity: true });
});
it('renders the statistics', () => {
expect(findUsage().exists()).toBe(true);
expect(findUsage().text()).toBe('4');
});
});
describe('with FF `ci_catalog_popularity` disabled', () => {
beforeEach(() => {
createComponent();
});
it('renders the statistics', () => {
expect(findUsage().exists()).toBe(false);
});
});
describe('when there are no statistics', () => {
it('render favorites as 0', () => {
it('render favorites and usage as 0', () => {
createComponent({
props: {
resource: {

View File

@ -95,6 +95,7 @@ export const catalogResponseBody = {
name: 'Project-42 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-42/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -111,6 +112,7 @@ export const catalogResponseBody = {
name: 'Project-41 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-41/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -127,6 +129,7 @@ export const catalogResponseBody = {
name: 'Project-40 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-40/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -143,6 +146,7 @@ export const catalogResponseBody = {
name: 'Project-39 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-39/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -159,6 +163,7 @@ export const catalogResponseBody = {
name: 'Project-38 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-38/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -175,6 +180,7 @@ export const catalogResponseBody = {
name: 'Project-37 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-37/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -191,6 +197,7 @@ export const catalogResponseBody = {
name: 'Project-36 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-36/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -207,6 +214,7 @@ export const catalogResponseBody = {
name: 'Project-35 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-35/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -223,6 +231,7 @@ export const catalogResponseBody = {
name: 'Project-34 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-34/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -239,6 +248,7 @@ export const catalogResponseBody = {
name: 'Project-33 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-33/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -255,6 +265,7 @@ export const catalogResponseBody = {
name: 'Project-32 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-32/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -271,6 +282,7 @@ export const catalogResponseBody = {
name: 'Project-31 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-31/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -287,6 +299,7 @@ export const catalogResponseBody = {
name: 'Project-30 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-30/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -303,6 +316,7 @@ export const catalogResponseBody = {
name: 'Project-29 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-29/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -319,6 +333,7 @@ export const catalogResponseBody = {
name: 'Project-28 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-28/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -335,6 +350,7 @@ export const catalogResponseBody = {
name: 'Project-27 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-27/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -351,6 +367,7 @@ export const catalogResponseBody = {
name: 'Project-26 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-26/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -367,6 +384,7 @@ export const catalogResponseBody = {
name: 'Project-25 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-25/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -383,6 +401,7 @@ export const catalogResponseBody = {
name: 'Project-24 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-24/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -399,6 +418,7 @@ export const catalogResponseBody = {
name: 'Project-23 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 0,
starrersPath: '/frontend-fixtures/project-23/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -434,6 +454,7 @@ export const catalogSinglePageResponse = {
name: 'Project-45 Name',
description: 'A simple component',
starCount: 0,
last30DayUsageCount: 4,
verificationLevel: 'UNVERIFIED',
versions: {
__typename: 'CiCatalogResourceVersionConnection',
@ -505,6 +526,7 @@ export const catalogSharedDataMock = {
description: 'This is the description of the repo',
name: 'Ruby',
starCount: 1,
last30DayUsageCount: 4,
starrersPath: '/path/to/project/-/starrers',
verificationLevel: 'UNVERIFIED',
versions: {
@ -543,6 +565,7 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
icon: 'my-icon',
name: `My component #${i}`,
starCount: 10,
last30DayUsageCount: 4,
versions: {
__typename: 'CiCatalogResourceVersionConnection',
nodes: [

View File

@ -365,7 +365,6 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: 'ID_DESC',
});
});
});
@ -427,7 +426,6 @@ describe('Pipeline schedules app', () => {
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: 'ID_DESC',
});
});
@ -441,7 +439,6 @@ describe('Pipeline schedules app', () => {
last: null,
prevPageCursor: '',
nextPageCursor: pageInfo.endCursor,
sortValue: 'ID_DESC',
});
expect(findPagination().props('value')).toEqual(2);
});
@ -459,50 +456,6 @@ describe('Pipeline schedules app', () => {
});
});
describe('when sorting changes', () => {
const newSort = 'DESCRIPTION_ASC';
beforeEach(async () => {
createComponent([[getPipelineSchedulesQuery, successHandler]]);
await waitForPromises();
await findTable().vm.$emit('update-sorting', newSort, 'description', false);
});
it('passes it to the graphql query', () => {
expect(successHandler).toHaveBeenCalledTimes(2);
expect(successHandler.mock.calls[1][0]).toEqual({
projectPath: 'gitlab-org/gitlab',
ids: null,
first: SCHEDULES_PER_PAGE,
last: null,
nextPageCursor: '',
prevPageCursor: '',
sortValue: newSort,
});
});
});
describe('when update-sorting event is emitted', () => {
beforeEach(async () => {
createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
await waitForPromises();
});
it('resets the page count', async () => {
expect(findPagination().props('value')).toEqual(1);
await setPage(2);
expect(findPagination().props('value')).toEqual(2);
await findTable().vm.$emit('update-sorting', 'DESCRIPTION_DESC', 'description', true);
await waitForPromises();
expect(findPagination().props('value')).toEqual(1);
});
});
it.each`
description | handler | buttonDisabled | alertExists
${'limit reached'} | ${planLimitReachedHandler} | ${true} | ${true}

View File

@ -1,89 +0,0 @@
import { GlTable } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import {
TH_DESCRIPTION_TEST_ID,
TH_TARGET_TEST_ID,
TH_NEXT_TEST_ID,
} from '~/ci/pipeline_schedules/constants';
import { mockPipelineScheduleNodes, mockPipelineScheduleCurrentUser } from '../mock_data';
describe('Pipeline schedules table', () => {
let wrapper;
const createComponent = () => {
wrapper = mountExtended(PipelineSchedulesTable, {
propsData: {
schedules: mockPipelineScheduleNodes,
currentUser: mockPipelineScheduleCurrentUser,
sortBy: 'ID',
sortDesc: true,
},
});
};
const findTable = () => wrapper.findComponent(GlTable);
describe('sorting', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it.each`
sortValue | sortBy | sortDesc
${'DESCRIPTION_ASC'} | ${'description'} | ${false}
${'DESCRIPTION_DESC'} | ${'description'} | ${true}
${'REF_ASC'} | ${'target'} | ${false}
${'REF_DESC'} | ${'target'} | ${true}
${'NEXT_RUN_AT_ASC'} | ${'next'} | ${false}
${'NEXT_RUN_AT_DESC'} | ${'next'} | ${true}
`(
'emits sort data in expected format for sortValue $sortValue',
({ sortValue, sortBy, sortDesc }) => {
findTable().vm.$emit('sort-changed', { sortBy, sortDesc });
expect(wrapper.emitted('update-sorting')[0]).toEqual([sortValue, sortBy, sortDesc]);
},
);
it('emits no update-sorting event when called with unsortable column', () => {
findTable().vm.$emit('sort-changed', { sortBy: 'actions', sortDesc: false });
expect(wrapper.emitted('update-sorting')).toBeUndefined();
});
it('emits no update-sorting event when called with unknown column', () => {
findTable().vm.$emit('sort-changed', { sortBy: 'not-defined-never', sortDesc: false });
expect(wrapper.emitted('update-sorting')).toBeUndefined();
});
});
describe('sorting the pipeline schedules table by column', () => {
beforeEach(async () => {
createComponent();
await waitForPromises();
});
it.each`
description | selector
${'description'} | ${TH_DESCRIPTION_TEST_ID}
${'target'} | ${TH_TARGET_TEST_ID}
${'next'} | ${TH_NEXT_TEST_ID}
`('updates sort with new direction when sorting by $description', async ({ selector }) => {
const [[attr, value]] = Object.entries(selector);
const columnHeader = () => wrapper.find(`[${attr}="${value}"]`);
expect(columnHeader().attributes('aria-sort')).toBe('none');
columnHeader().trigger('click');
await waitForPromises();
expect(columnHeader().attributes('aria-sort')).toBe('ascending');
columnHeader().trigger('click');
await waitForPromises();
expect(columnHeader().attributes('aria-sort')).toBe('descending');
});
});
});

View File

@ -135,98 +135,128 @@ describe('kubernetes_logs', () => {
});
describe('when environment data is ready', () => {
describe('when logs data is empty', () => {
beforeEach(async () => {
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
mountComponent();
await waitForPromises();
describe('when no container is specified for the logs', () => {
describe('when logs data is empty', () => {
beforeEach(async () => {
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
mountComponent();
await waitForPromises();
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not render error state', () => {
expect(findAlert().exists()).toBe(false);
});
it('should not render logs viewer', () => {
expect(findLogsViewer().exists()).toBe(false);
});
it('should render empty state with pod name', () => {
expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
});
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
describe('when logs data fetched successfully', () => {
beforeEach(async () => {
mountComponent();
await waitForPromises();
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not render error state', () => {
expect(findAlert().exists()).toBe(false);
});
it('should not render empty state', () => {
expect(findEmptyState().exists()).toBe(false);
});
it('should query logs', () => {
expect(k8sLogsQueryMock).toHaveBeenCalledWith(
expect.anything(),
{
configuration,
namespace: defaultProps.namespace,
podName: defaultProps.podName,
containerName: '',
},
expect.anything(),
expect.anything(),
);
});
it('should render logs viewer component with correct parameters', () => {
const expectedLogLines = [
{
content: [{ text: logsMockData[0].content }],
lineNumber: 1,
lineId: 'L1',
},
{
content: [{ text: logsMockData[1].content }],
lineNumber: 2,
lineId: 'L2',
},
];
expect(findLogsViewer().props()).toMatchObject({
logLines: expectedLogLines,
highlightedLine: 'L2',
});
});
});
it('should not render error state', () => {
expect(findAlert().exists()).toBe(false);
});
it('should not render logs viewer', () => {
expect(findLogsViewer().exists()).toBe(false);
});
it('should render empty state', () => {
expect(findEmptyState().text()).toBe('No logs available for pod test-pod');
describe('when logs data fetch failed', () => {
const errorMessage = 'Error while fetching logs';
beforeEach(async () => {
k8sLogsQueryMock = jest.fn().mockResolvedValue({
error: { message: errorMessage },
});
mountComponent();
await waitForPromises();
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should render error state', () => {
expect(findAlert().text()).toBe(errorMessage);
});
it('should render empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('should not render logs viewer', () => {
expect(findLogsViewer().exists()).toBe(false);
});
});
});
describe('when logs data fetched successfully', () => {
describe('when a container is specified for the logs', () => {
beforeEach(async () => {
mountComponent();
k8sLogsQueryMock = jest.fn().mockResolvedValue({});
mountComponent({ containerName: 'my-container' });
await waitForPromises();
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should not render error state', () => {
expect(findAlert().exists()).toBe(false);
});
it('should not render empty state', () => {
expect(findEmptyState().exists()).toBe(false);
it('should render empty state with pod and container name', () => {
expect(findEmptyState().text()).toBe(
'No logs available for container my-container of pod test-pod',
);
});
it('should query logs', () => {
it('should query logs with the container name included', () => {
expect(k8sLogsQueryMock).toHaveBeenCalledWith(
expect.anything(),
{
configuration,
namespace: defaultProps.namespace,
podName: defaultProps.podName,
containerName: 'my-container',
},
expect.anything(),
expect.anything(),
);
});
it('should render logs viewer component with correct parameters', () => {
const expectedLogLines = [
{
content: [{ text: logsMockData[0].content }],
lineNumber: 1,
lineId: 'L1',
},
{
content: [{ text: logsMockData[1].content }],
lineNumber: 2,
lineId: 'L2',
},
];
expect(findLogsViewer().props()).toMatchObject({
logLines: expectedLogLines,
highlightedLine: 'L2',
});
});
});
describe('when logs data fetch failed', () => {
const errorMessage = 'Error while fetching logs';
beforeEach(async () => {
k8sLogsQueryMock = jest.fn().mockResolvedValue({
error: { message: errorMessage },
});
mountComponent();
await waitForPromises();
});
it('should not render loading state', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
it('should render error state', () => {
expect(findAlert().text()).toBe(errorMessage);
});
it('should render empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('should not render logs viewer', () => {
expect(findLogsViewer().exists()).toBe(false);
});
});
});
});

View File

@ -30,6 +30,27 @@ describe('k8sLogs', () => {
watchStream = bootstrapWatcherMock();
});
it('should request pods logs if no container is specified', async () => {
await k8sLogs(null, { configuration, namespace, podName }, { client });
expect(
watchStream.subscribeToStreamMock,
).toHaveBeenCalledWith('/api/v1/namespaces/default/pods/test-pod/log', { follow: true });
});
it('should request specific container logs if container is specified', async () => {
const containerName = 'my-container';
await k8sLogs(null, { configuration, namespace, podName, containerName }, { client });
expect(watchStream.subscribeToStreamMock).toHaveBeenCalledWith(
'/api/v1/namespaces/default/pods/test-pod/log',
{
follow: true,
container: containerName,
},
);
});
const errorMessage = 'event error message';
const logContent = 'Plain text log data';
it.each([

View File

@ -0,0 +1,51 @@
import { GlAlert, GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import WebIdeError from '~/ide/components/web_ide_error.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
const findButtons = (wrapper) => wrapper.findAllComponents(GlButton);
describe('WebIdeError', () => {
const MOCK_SIGN_OUT_PATH = '/users/sign_out';
let wrapper;
useMockLocationHelper();
function createWrapper() {
wrapper = mount(WebIdeError, {
propsData: {
signOutPath: MOCK_SIGN_OUT_PATH,
},
});
}
it('renders alert component', () => {
createWrapper();
const alert = wrapper.findComponent(GlAlert);
expect(alert.text()).toMatchInterpolatedText(
'Failed to load the Web IDE For more information, see the developer console. Try to reload the page or sign out and in again. If the issue persists, report a problem. Reload Sign out',
);
});
it('renders reload page button', () => {
createWrapper();
const reloadButton = findButtons(wrapper).at(0);
expect(reloadButton.text()).toEqual('Reload');
reloadButton.vm.$emit('click');
expect(window.location.reload).toHaveBeenCalled();
});
it('renders sign out button', () => {
createWrapper();
const signOutButton = findButtons(wrapper).at(1);
expect(signOutButton.text()).toEqual('Sign out');
expect(signOutButton.attributes()).toMatchObject({
'data-method': 'post',
href: MOCK_SIGN_OUT_PATH,
});
});
});

View File

@ -8,6 +8,7 @@ import Tracking from '~/tracking';
import { TEST_HOST } from 'helpers/test_constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { renderWebIdeError } from '~/ide/render_web_ide_error';
import { getMockCallbackUrl } from './helpers';
jest.mock('@gitlab/web-ide');
@ -18,6 +19,7 @@ jest.mock('~/lib/utils/csrf', () => ({
headerKey: 'mock-csrf-header',
}));
jest.mock('~/tracking');
jest.mock('~/ide/render_web_ide_error');
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
@ -30,6 +32,7 @@ const TEST_FILE_PATH = 'foo/README.md';
const TEST_MR_ID = '7';
const TEST_MR_TARGET_PROJECT = 'gitlab-org/the-real-gitlab';
const TEST_SIGN_IN_PATH = 'sign-in';
const TEST_SIGN_OUT_PATH = 'sign-out';
const TEST_FORK_INFO = { fork_path: '/forky' };
const TEST_IDE_REMOTE_PATH = '/-/ide/remote/:remote_host/:remote_path';
const TEST_START_REMOTE_PARAMS = {
@ -82,6 +85,7 @@ describe('ide/init_gitlab_web_ide', () => {
],
});
el.dataset.signInPath = TEST_SIGN_IN_PATH;
el.dataset.signOutPath = TEST_SIGN_OUT_PATH;
document.body.append(el);
};
@ -272,6 +276,27 @@ describe('ide/init_gitlab_web_ide', () => {
});
});
describe('on start error', () => {
const mockError = new Error('error');
beforeEach(() => {
jest.mocked(start).mockImplementationOnce(() => {
throw mockError;
});
createSubject();
});
it('shows alert', () => {
expect(start).toHaveBeenCalledTimes(1);
expect(renderWebIdeError).toHaveBeenCalledTimes(1);
expect(renderWebIdeError).toHaveBeenCalledWith({
error: mockError,
signOutPath: TEST_SIGN_OUT_PATH,
});
});
});
describe('when extensionsGallerySettings is in dataset', () => {
function setMockExtensionGallerySettingsDataset(
mockSettings = TEST_EXTENSIONS_GALLERY_SETTINGS,

View File

@ -0,0 +1,55 @@
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { renderWebIdeError } from '~/ide/render_web_ide_error';
import { logError } from '~/lib/logger';
import { resetHTMLFixture } from 'helpers/fixtures';
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/lib/logger');
describe('render web IDE error', () => {
const MOCK_ERROR = new Error('error');
const MOCK_SIGNOUT_PATH = '/signout';
const setupFlashContainer = () => {
const flashContainer = document.createElement('div');
flashContainer.classList.add('flash-container');
document.body.appendChild(flashContainer);
};
const findAlert = () => document.querySelector('.flash-container .gl-alert');
afterEach(() => {
resetHTMLFixture();
});
describe('with flash-container', () => {
beforeEach(() => {
setupFlashContainer();
renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
});
it('logs error to Sentry', () => {
expect(Sentry.captureException).toHaveBeenCalledWith(MOCK_ERROR);
});
it('logs error to console', () => {
expect(logError).toHaveBeenCalledWith('Failed to load Web IDE', MOCK_ERROR);
});
it('should render alert', () => {
expect(findAlert()).toBeInstanceOf(HTMLElement);
});
});
describe('no .flash-container', () => {
beforeEach(() => {
renderWebIdeError({ error: MOCK_ERROR, signOutPath: MOCK_SIGNOUT_PATH });
});
it('does not render alert', () => {
expect(findAlert()).toBeNull();
});
});
});

View File

@ -57,6 +57,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to match_array(["'test'"]) }
end
context 'when exclude_refs is provided' do
let(:opts) { { exclude_refs: ['master'] } }
it { is_expected.not_to include('master') }
end
context 'with limit + exclude_refs options' do
let(:opts) { { limit: 1, exclude_refs: ["'test'"] } }
it { is_expected.to match_array(["2-mb-file"]) }
end
describe 'when storage is broken', :broken_storage do
it 'raises a storage error' do
expect_to_raise_storage_error do
@ -79,6 +91,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
it { is_expected.to match_array(['v1.1.0']) }
end
context 'when exclude_refs is provided' do
let(:opts) { { exclude_refs: ['v1.1.0'] } }
it { is_expected.not_to include('v1.1.0') }
end
context 'with limit + exclude_refs options' do
let(:opts) { { limit: 1, exclude_refs: ["v1.1.0"] } }
it { is_expected.to match_array(["v1.1.1"]) }
end
end
describe '#tags_sorted_by' do

View File

@ -1377,37 +1377,6 @@ RSpec.describe User, feature_category: :user_profile do
end
end
describe '.with_expiring_and_not_notified_personal_access_tokens' do
let_it_be(:user1) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:user3) { create(:user) }
let_it_be(:expired_token) { create(:personal_access_token, user: user1, expires_at: 2.days.ago) }
let_it_be(:revoked_token) { create(:personal_access_token, user: user1, revoked: true) }
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user1, expires_at: 2.days.from_now) }
let_it_be(:valid_token_and_notified) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now, expire_notification_delivered: true) }
let_it_be(:valid_token1) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
let_it_be(:valid_token2) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
let(:users) { described_class.with_expiring_and_not_notified_personal_access_tokens(from) }
context 'in one day' do
let(:from) { 1.day.from_now }
it "doesn't include an user" do
expect(users).to be_empty
end
end
context 'in three days' do
let(:from) { 3.days.from_now }
it 'only includes user2' do
expect(users).to contain_exactly(user2)
end
end
end
describe '.with_personal_access_tokens_expired_today' do
let_it_be(:user1) { create(:user) }
let_it_be(:expired_today) { create(:personal_access_token, user: user1, expires_at: Date.current) }

View File

@ -896,6 +896,22 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
describe 'GET /api/v4/jobs/:id/artifacts' do
let(:token) { job.token }
def expect_use_primary
lb_session = ::Gitlab::Database::LoadBalancing::Session.current
expect(lb_session).to receive(:use_primary).and_call_original
allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
end
def expect_no_use_primary
lb_session = ::Gitlab::Database::LoadBalancing::Session.current
expect(lb_session).not_to receive(:use_primary)
allow(::Gitlab::Database::LoadBalancing::Session).to receive(:current).and_return(lb_session)
end
it_behaves_like 'API::CI::Runner application context metadata', 'GET /api/:version/jobs/:id/artifacts' do
let(:send_request) { download_artifact }
end
@ -927,13 +943,32 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state, feature_catego
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
end
it 'downloads artifacts' do
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is on' do
it 'downloads artifacts' do
expect_use_primary
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
download_artifact
download_artifact
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include download_headers
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include download_headers
end
end
context 'when ci_job_artifacts_use_primary_to_authenticate feature flag is off' do
before do
stub_feature_flags(ci_job_artifacts_use_primary_to_authenticate: false)
end
it 'downloads artifacts' do
expect_no_use_primary
expect(Gitlab::ApplicationContext).to receive(:push).with(artifact: an_instance_of(Ci::JobArtifact)).once.and_call_original
download_artifact
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers.to_h).to include download_headers
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::PackagesController, feature_category: :package_registry do
let_it_be(:group) { create(:group, :public) }
let_it_be(:project) { create(:project, :public, group: group) }
describe 'GET #show' do
let_it_be(:package) { create(:package, project: project) }
subject do
get group_package_path(group_id: group.full_path, id: package.id)
response
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: true)) }
context 'when feature flag "packages_protected_packages" is disabled' do
before do
stub_feature_flags(packages_protected_packages: false)
end
it { is_expected.to have_gitlab_http_status(:ok) }
it { is_expected.to have_attributes(body: have_pushed_frontend_feature_flags(packagesProtectedPackages: false)) }
end
end
end

View File

@ -5,7 +5,7 @@ require_relative 'helpers/wait_for_requests'
module Capybara
class Session
module WaitForAllRequestsAfterVisitPage
module WaitForRequestsAfterVisitPage
include CapybaraHelpers
include WaitForRequests
@ -14,11 +14,11 @@ module Capybara
yield if block
wait_for_all_requests
wait_for_requests
end
end
prepend WaitForAllRequestsAfterVisitPage
prepend WaitForRequestsAfterVisitPage
end
module Node

View File

@ -181,8 +181,6 @@ RSpec.shared_examples 'work items assignees' do
end
it 'updates the assignee in real-time' do
Capybara::Session.new(:other_session)
using_session :other_session do
visit work_items_path
expect(work_item.reload.assignees).not_to include(user)
@ -240,8 +238,6 @@ RSpec.shared_examples 'work items labels' do
end
it 'updates the assignee in real-time' do
Capybara::Session.new(:other_session)
using_session :other_session do
visit work_items_path
expect(work_item.reload.labels).not_to include(label)

View File

@ -2,22 +2,22 @@
require 'fast_spec_helper'
require 'capybara'
require 'support/capybara_wait_for_all_requests'
require 'support/capybara_wait_for_requests'
RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do # rubocop:disable RSpec/FilePath
context 'for Capybara::Session::WaitForAllRequestsAfterVisitPage' do
RSpec.describe 'capybara_wait_for_requests', feature_category: :tooling do
context 'for Capybara::Session::WaitForRequestsAfterVisitPage' do
let(:page_visitor) do
Class.new do
def visit(visit_uri)
visit_uri
end
prepend Capybara::Session::WaitForAllRequestsAfterVisitPage
prepend Capybara::Session::WaitForRequestsAfterVisitPage
end.new
end
it 'waits for all requests after a page visit' do
expect(page_visitor).to receive(:wait_for_all_requests)
it 'waits for requests after a page visit' do
expect(page_visitor).to receive(:wait_for_requests)
page_visitor.visit('http://test.com')
end

View File

@ -130,14 +130,87 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker, feature_cate
context 'when a token is owned by a group bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
let_it_be(:group) { create(:group) }
let_it_be(:expiring_token) { create(:personal_access_token, user: project_bot, expires_at: 5.days.from_now) }
before_all do
group.add_developer(project_bot)
context 'when the group of the resource bot exists' do
let_it_be(:group) { create(:group) }
before_all do
group.add_maintainer(project_bot)
end
it_behaves_like 'sends notification about expiry of bot user tokens'
it 'updates expire notification delivered attribute of the token' do
expect { worker.perform }.to change { expiring_token.reload.expire_notification_delivered }.from(false).to(true)
end
context 'when exception is raised during processing' do
context 'with a single resource access token' do
before do
allow_next_instance_of(NotificationService) do |service|
allow(service).to(
receive(:bot_resource_access_token_about_to_expire)
.with(project_bot, expiring_token.name)
.and_raise('boom!')
)
end
end
it 'logs error' do
expect(Gitlab::AppLogger).to(
receive(:error)
.with({ message: 'Failed to send notification about expiring resource access tokens',
class: described_class,
"exception.class": "RuntimeError",
"exception.message": "boom!",
user_id: project_bot.id })
)
worker.perform
end
it 'does not update token with failed delivery' do
expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
end
end
context 'with multiple resource access tokens' do
let_it_be(:another_project_bot) { create(:user, :project_bot) }
let_it_be(:another_expiring_token) { create(:personal_access_token, user: another_project_bot, expires_at: 5.days.from_now) }
before_all do
group.add_maintainer(another_project_bot)
end
it 'continues sending email' do
expect_next_instance_of(NotificationService) do |service|
expect(service).to(
receive(:bot_resource_access_token_about_to_expire)
.with(project_bot, expiring_token.name)
.and_raise('boom!')
)
expect(service).to(
receive(:bot_resource_access_token_about_to_expire)
.with(another_project_bot, another_expiring_token.name)
.and_call_original
)
end
worker.perform
end
end
end
end
it_behaves_like 'sends notification about expiry of bot user tokens'
context 'when the group of the resource bot has been deleted' do
it 'does not update token with no delivery' do
expect(Group).to be_none
expect(Project).to be_none
expect { worker.perform }.not_to change { expiring_token.reload.expire_notification_delivered }
end
end
end
end
end