Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4e81d9c050
commit
57d1bb8254
|
@ -32,7 +32,6 @@ rules:
|
|||
no-else-return:
|
||||
- error
|
||||
- allowElseIf: true
|
||||
import/no-useless-path-segments: off
|
||||
lines-between-class-members: off
|
||||
# Disabled for now, to make the plugin-vue 4.5 -> 5.0 update smoother
|
||||
vue/no-confusing-v-for-v-if: error
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.run-dev-fixtures:
|
||||
extends:
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .rails-cache
|
||||
- .default-before_script
|
||||
- .use-pg11
|
||||
stage: test
|
||||
|
@ -19,8 +19,9 @@ run-dev-fixtures:
|
|||
- .run-dev-fixtures
|
||||
- .dev-fixtures:rules:ee-and-foss
|
||||
script:
|
||||
- scripts/gitaly-test-spawn
|
||||
- RAILS_ENV=test bundle exec rake db:seed_fu
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- run_timed_command "RAILS_ENV=test bundle exec rake db:seed_fu"
|
||||
|
||||
run-dev-fixtures-ee:
|
||||
extends:
|
||||
|
@ -28,6 +29,7 @@ run-dev-fixtures-ee:
|
|||
- .dev-fixtures:rules:ee-only
|
||||
- .use-pg11-ee
|
||||
script:
|
||||
- scripts/gitaly-test-spawn
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- cp ee/db/fixtures/development/* $FIXTURE_PATH
|
||||
- RAILS_ENV=test bundle exec rake db:seed_fu
|
||||
- run_timed_command "RAILS_ENV=test bundle exec rake db:seed_fu"
|
||||
|
|
|
@ -136,16 +136,15 @@ compile-assets pull-cache as-if-foss:
|
|||
.frontend-fixtures-base:
|
||||
extends:
|
||||
- .default-retry
|
||||
- .default-cache
|
||||
- .rails-cache
|
||||
- .default-before_script
|
||||
- .use-pg11
|
||||
stage: fixtures
|
||||
needs: ["setup-test-env", "compile-assets pull-cache"]
|
||||
script:
|
||||
- date
|
||||
- scripts/gitaly-test-spawn
|
||||
- date
|
||||
- bundle exec rake frontend:fixtures
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- run_timed_command "bundle exec rake frontend:fixtures"
|
||||
artifacts:
|
||||
name: frontend-fixtures
|
||||
expire_in: 31d
|
||||
|
|
|
@ -29,6 +29,19 @@
|
|||
- vendor/gitaly-ruby
|
||||
policy: pull
|
||||
|
||||
.rails-cache:
|
||||
cache:
|
||||
key:
|
||||
files:
|
||||
- Gemfile.lock
|
||||
- GITALY_SERVER_VERSION
|
||||
prefix: "ruby-go-cache-v1"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- vendor/gitaly-ruby
|
||||
- .go/pkg/mod
|
||||
policy: pull
|
||||
|
||||
.yarn-cache:
|
||||
cache:
|
||||
key:
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
.rails:needs:setup-and-assets:
|
||||
needs: ["setup-test-env", "compile-assets pull-cache"]
|
||||
|
||||
.rails-cache:
|
||||
cache:
|
||||
key: "ruby-go-cache-v1"
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- vendor/gitaly-ruby
|
||||
- .go/pkg/mod
|
||||
policy: pull
|
||||
|
||||
.rails-job-base:
|
||||
extends:
|
||||
- .default-retry
|
||||
|
@ -18,15 +9,18 @@
|
|||
|
||||
#######################################################
|
||||
# EE/FOSS: default refs (MRs, master, schedules) jobs #
|
||||
.base-setup-test-env:
|
||||
setup-test-env:
|
||||
extends:
|
||||
- .rails-job-base
|
||||
- .rails:rules:default-refs-code-backstage-qa
|
||||
- .use-pg11
|
||||
stage: prepare
|
||||
variables:
|
||||
GITLAB_TEST_EAGER_LOAD: "0"
|
||||
script:
|
||||
- run_timed_command "bundle exec ruby -I. -e 'require \"config/environment\"; TestEnv.init'"
|
||||
- run_timed_command "scripts/gitaly-test-build" # Do not use 'bundle exec' here
|
||||
- rm tmp/tests/gitaly/.ruby-bundle # This file prevents gems from being installed even if vendor/gitaly-ruby is missing
|
||||
artifacts:
|
||||
expire_in: 7d
|
||||
paths:
|
||||
|
@ -44,12 +38,6 @@
|
|||
cache:
|
||||
policy: pull-push
|
||||
|
||||
setup-test-env:
|
||||
extends:
|
||||
- .base-setup-test-env
|
||||
- .rails:rules:default-refs-code-backstage-qa
|
||||
- .use-pg11
|
||||
|
||||
static-analysis:
|
||||
extends:
|
||||
- .rails-job-base
|
||||
|
@ -84,6 +72,8 @@ downtime_check:
|
|||
stage: test
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-assets pull-cache"]
|
||||
script:
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- source scripts/rspec_helpers.sh
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
|
||||
artifacts:
|
||||
|
@ -108,6 +98,8 @@ downtime_check:
|
|||
|
||||
.rspec-base-migration:
|
||||
script:
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- source scripts/rspec_helpers.sh
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag level:migration"
|
||||
|
||||
|
@ -193,6 +185,7 @@ gitlab:setup:
|
|||
# db/fixtures/development/04_project.rb thanks to SIZE=1 below
|
||||
- git clone https://gitlab.com/gitlab-org/gitlab-test.git
|
||||
/home/git/repositories/gitlab-org/gitlab-test.git
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- force=yes SIZE=1 FIXTURE_PATH="db/fixtures/development" bundle exec rake gitlab:setup
|
||||
artifacts:
|
||||
|
@ -300,6 +293,8 @@ rspec-ee system pg11:
|
|||
.rspec-ee-base-geo:
|
||||
extends: .rspec-base-ee
|
||||
script:
|
||||
- run_timed_command "scripts/gitaly-test-build"
|
||||
- run_timed_command "scripts/gitaly-test-spawn"
|
||||
- source scripts/rspec_helpers.sh
|
||||
- scripts/prepare_postgres_fdw.sh
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag geo"
|
||||
|
|
|
@ -466,8 +466,12 @@ Rails/TimeZone:
|
|||
Enabled: true
|
||||
EnforcedStyle: 'flexible'
|
||||
Include:
|
||||
- 'app/controllers/**/*'
|
||||
- 'app/services/**/*'
|
||||
- 'spec/controllers/**/*'
|
||||
- 'spec/services/**/*'
|
||||
- 'ee/app/controllers/**/*'
|
||||
- 'ee/app/services/**/*'
|
||||
- 'ee/spec/controllers/**/*'
|
||||
- 'ee/spec/services/**/*'
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
GlButton,
|
||||
} from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { s__ } from '~/locale';
|
||||
import query from '../graphql/queries/details.query.graphql';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
|
@ -23,9 +22,9 @@ import updateAlertStatus from '../graphql/mutations/update_alert_status.graphql'
|
|||
|
||||
export default {
|
||||
statuses: {
|
||||
triggered: s__('AlertManagement|Triggered'),
|
||||
acknowledged: s__('AlertManagement|Acknowledged'),
|
||||
resolved: s__('AlertManagement|Resolved'),
|
||||
TRIGGERED: s__('AlertManagement|Triggered'),
|
||||
ACKNOWLEDGED: s__('AlertManagement|Acknowledged'),
|
||||
RESOLVED: s__('AlertManagement|Resolved'),
|
||||
},
|
||||
i18n: {
|
||||
errorMsg: s__(
|
||||
|
@ -100,7 +99,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
capitalizeFirstCharacter,
|
||||
dismissError() {
|
||||
this.isErrorDismissed = true;
|
||||
},
|
||||
|
@ -177,11 +175,7 @@ export default {
|
|||
>
|
||||
<h2 data-testid="title">{{ alert.title }}</h2>
|
||||
</div>
|
||||
<gl-dropdown
|
||||
:text="capitalizeFirstCharacter(alert.status.toLowerCase())"
|
||||
class="gl-absolute gl-right-0"
|
||||
right
|
||||
>
|
||||
<gl-dropdown :text="$options.statuses[alert.status]" class="gl-absolute gl-right-0" right>
|
||||
<gl-dropdown-item
|
||||
v-for="(label, field) in $options.statuses"
|
||||
:key="field"
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
#import "./listItem.fragment.graphql"
|
||||
|
||||
fragment AlertDetailItem on AlertManagementAlert {
|
||||
...AlertListItem
|
||||
createdAt
|
||||
monitoringTool
|
||||
service
|
||||
description
|
||||
updatedAt
|
||||
details
|
||||
}
|
|
@ -1,17 +1,10 @@
|
|||
#import "../fragments/detailItem.fragment.graphql"
|
||||
|
||||
query alertDetails($fullPath: ID!, $alertId: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
alertManagementAlerts(iid: $alertId) {
|
||||
nodes {
|
||||
iid
|
||||
createdAt
|
||||
endedAt
|
||||
eventCount
|
||||
monitoringTool
|
||||
service
|
||||
severity
|
||||
startedAt
|
||||
status
|
||||
title
|
||||
...AlertDetailItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
|
||||
import EnvironmentTable from '../components/environments_table.vue';
|
||||
import EnvironmentTable from './environments_table.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -52,7 +52,7 @@ export default {
|
|||
footer-primary-button-variant="danger"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template slot="header">
|
||||
<template #header>
|
||||
<h4 class="modal-title d-flex mw-100">
|
||||
{{ __('Delete') }}
|
||||
<span v-gl-tooltip :title="environment.name" class="text-truncate mx-1 flex-fill">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import service from './../services';
|
||||
import service from '../services';
|
||||
import * as types from './mutation_types';
|
||||
import createFlash from '~/flash';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mapState, mapActions, mapGetters } from 'vuex';
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
import eventHub from '../event_hub';
|
||||
import store from '../store/';
|
||||
import store from '../store';
|
||||
import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
|
||||
import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
|
||||
import FrequentItemsSearchInput from './frequent_items_search_input.vue';
|
||||
|
|
|
@ -4,7 +4,7 @@ import icon from '~/vue_shared/components/icon.vue';
|
|||
import upload from './upload.vue';
|
||||
import ItemButton from './button.vue';
|
||||
import { modalTypes } from '../../constants';
|
||||
import NewModal from '../new_dropdown/modal.vue';
|
||||
import NewModal from './modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { isNewJobLogActive } from '../store/utils';
|
||||
import { isNewJobLogActive } from './utils';
|
||||
|
||||
export default () => ({
|
||||
jobEndpoint: null,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { mapGetters, mapActions, mapState } from 'vuex';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
import eventHub from '../../notes/event_hub';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
/**
|
||||
* @param {string} selector
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits';
|
||||
import initUserInternalRegexPlaceholder from '../account_and_limits';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder());
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import { GlToast } from '@gitlab/ui';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import store from './store/';
|
||||
import store from './store';
|
||||
import RegistrySettingsApp from './components/registry_settings_app.vue';
|
||||
|
||||
Vue.use(GlToast);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue';
|
||||
import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue';
|
||||
import CollapsedAssigneeList from './collapsed_assignee_list.vue';
|
||||
import UncollapsedAssigneeList from './uncollapsed_assignee_list.vue';
|
||||
|
||||
export default {
|
||||
// name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import PublishToolbar from '../components/publish_toolbar.vue';
|
||||
import EditHeader from '../components/edit_header.vue';
|
||||
import PublishToolbar from './publish_toolbar.vue';
|
||||
import EditHeader from './edit_header.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
|
||||
import Flash from '../../../flash';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import MrWidgetAuthor from '../../components/mr_widget_author.vue';
|
||||
import MrWidgetAuthor from '../mr_widget_author.vue';
|
||||
import eventHub from '../../event_hub';
|
||||
import { AUTO_MERGE_STRATEGIES } from '../../constants';
|
||||
import { __ } from '~/locale';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
|
||||
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
|
||||
export default {
|
||||
|
|
|
@ -5,7 +5,7 @@ import Flash from '~/flash';
|
|||
import tooltip from '~/vue_shared/directives/tooltip';
|
||||
import { s__, __ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue';
|
||||
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
|
||||
import statusIcon from '../mr_widget_status_icon.vue';
|
||||
import eventHub from '../../event_hub';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import Icon from './icon.vue';
|
||||
|
||||
/**
|
||||
* Renders CI icon based on API response shared between all places where it is used.
|
||||
|
|
|
@ -4,7 +4,7 @@ import { GlTooltipDirective, GlLink } from '@gitlab/ui';
|
|||
import { __, sprintf } from '~/locale';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import UserAvatarLink from './user_avatar/user_avatar_link.vue';
|
||||
import Icon from '../../vue_shared/components/icon.vue';
|
||||
import Icon from './icon.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script>
|
||||
import fuzzaldrinPlus from 'fuzzaldrin-plus';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import FileIcon from '../../../vue_shared/components/file_icon.vue';
|
||||
import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue';
|
||||
import FileIcon from '../file_icon.vue';
|
||||
import ChangedFileIcon from '../changed_file_icon.vue';
|
||||
|
||||
const MAX_PATH_LENGTH = 60;
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { GlLink } from '@gitlab/ui';
|
||||
import { escape } from 'lodash';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import icon from '../../../vue_shared/components/icon.vue';
|
||||
import icon from '../icon.vue';
|
||||
|
||||
function buildDocsLinkStart(path) {
|
||||
return `<a href="${escape(path)}" target="_blank" rel="noopener noreferrer">`;
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import '~/commons/bootstrap';
|
||||
import { GlIcon, GlTooltip, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { sprintf } from '~/locale';
|
||||
import IssueMilestone from '../../components/issue/issue_milestone.vue';
|
||||
import IssueAssignees from '../../components/issue/issue_assignees.vue';
|
||||
import IssueMilestone from './issue_milestone.vue';
|
||||
import IssueAssignees from './issue_assignees.vue';
|
||||
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
|
||||
import CiIcon from '../ci_icon.vue';
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ class Import::GithubController < Import::BaseController
|
|||
end
|
||||
|
||||
def provider_rate_limit(exception)
|
||||
reset_time = Time.at(exception.response_headers['x-ratelimit-reset'].to_i)
|
||||
reset_time = Time.zone.at(exception.response_headers['x-ratelimit-reset'].to_i)
|
||||
session[access_token_key] = nil
|
||||
redirect_to new_import_url,
|
||||
alert: _("GitHub API rate limit exceeded. Try again after %{reset_time}") % { reset_time: reset_time }
|
||||
|
|
|
@ -58,7 +58,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
|
||||
|
||||
if @project.saved?
|
||||
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
|
||||
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.zone.at(0) }
|
||||
|
||||
redirect_to(
|
||||
project_path(@project, custom_import_params),
|
||||
|
|
|
@ -5,13 +5,10 @@ class ApplicationSetting < ApplicationRecord
|
|||
include CacheMarkdownField
|
||||
include TokenAuthenticatable
|
||||
include ChronicDurationAttribute
|
||||
include IgnorableColumns
|
||||
|
||||
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
|
||||
'Admin Area > Settings > Metrics and profiling > Metrics - Grafana'
|
||||
|
||||
ignore_column :elasticsearch_experimental_indexer, remove_with: '13.1', remove_after: '2020-05-22'
|
||||
|
||||
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
|
||||
add_authentication_token_field :health_check_access_token
|
||||
add_authentication_token_field :static_objects_external_storage_auth_token
|
||||
|
|
|
@ -24,6 +24,7 @@ module HasUserType
|
|||
scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
|
||||
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
|
||||
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
|
||||
scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
|
||||
|
||||
enum user_type: USER_TYPES
|
||||
|
||||
|
|
|
@ -17,6 +17,11 @@ class ProjectMember < Member
|
|||
.where('projects.namespace_id in (?)', groups.select(:id))
|
||||
end
|
||||
|
||||
scope :without_project_bots, -> do
|
||||
left_join_users
|
||||
.merge(User.without_project_bot)
|
||||
end
|
||||
|
||||
class << self
|
||||
# Add users to projects with passed access option
|
||||
#
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Foreign Key on projects.namespaces_id
|
||||
merge_request: 31675
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove elasticsearch_experimental_indexer column
|
||||
merge_request: 30628
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add fields to Alert Details view
|
||||
merge_request: 32392
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update css-loader ^1.0.0 -> ^2.1.1
|
||||
merge_request: 31743
|
||||
author: Pirate Praveen
|
||||
type: other
|
|
@ -232,7 +232,8 @@ module.exports = {
|
|||
{
|
||||
loader: 'css-loader',
|
||||
options: {
|
||||
name: '[name].[contenthash:8].[ext]',
|
||||
modules: 'global',
|
||||
localIdentName: '[name].[contenthash:8].[ext]',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexNonRequestedProjectMembersOnSourceIdSourceType < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(:members, [:source_id, :source_type], where: "requested_at IS NULL and type = 'ProjectMember'", name: 'index_non_requested_project_members_on_source_id_and_type')
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:members, 'index_non_requested_project_members_on_source_id_and_type')
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
class RemoveElasticExperimentalIndexerFromApplicationSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
remove_column :application_settings, :elasticsearch_experimental_indexer, :boolean
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddProjectsForeignKeyToNamespaces < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
FK_NAME = 'fk_projects_namespace_id'
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
add_foreign_key(
|
||||
:projects,
|
||||
:namespaces,
|
||||
column: :namespace_id,
|
||||
on_delete: :restrict,
|
||||
validate: false,
|
||||
name: FK_NAME
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists :projects, column: :namespace_id, name: FK_NAME
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,263 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop:disable Migration/PreventStrings
|
||||
|
||||
# This migration cleans up Projects that were orphaned when their namespace was deleted
|
||||
# Instead of deleting them, we:
|
||||
# - Find (or create) the Ghost User
|
||||
# - Create (if not already exists) a `lost-and-found` group owned by the Ghost User
|
||||
# - Find orphaned projects --> namespace_id can not be found in namespaces
|
||||
# - Move the orphaned projects to the `lost-and-found` group
|
||||
# (while making them private and setting `archived=true`)
|
||||
#
|
||||
# On GitLab.com (2020-05-11) this migration will update 66 orphaned projects
|
||||
class CleanupProjectsWithMissingNamespace < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
VISIBILITY_PRIVATE = 0
|
||||
ACCESS_LEVEL_OWNER = 50
|
||||
|
||||
# The batch size of projects to check in each iteration
|
||||
# We expect the selectivity for orphaned projects to be very low:
|
||||
# (66 orphaned projects out of a total 13.6M)
|
||||
# so 10K should be a safe choice
|
||||
BATCH_SIZE = 10000
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
class UserDetail < ActiveRecord::Base
|
||||
self.table_name = 'user_details'
|
||||
|
||||
belongs_to :user, class_name: 'CleanupProjectsWithMissingNamespace::User'
|
||||
end
|
||||
|
||||
class User < ActiveRecord::Base
|
||||
self.table_name = 'users'
|
||||
|
||||
LOST_AND_FOUND_GROUP = 'lost-and-found'
|
||||
USER_TYPE_GHOST = 5
|
||||
DEFAULT_PROJECTS_LIMIT = 100000
|
||||
|
||||
default_value_for :admin, false
|
||||
default_value_for :can_create_group, true # we need this to create the group
|
||||
default_value_for :can_create_team, false
|
||||
default_value_for :project_view, :files
|
||||
default_value_for :notified_of_own_activity, false
|
||||
default_value_for :preferred_language, I18n.default_locale
|
||||
|
||||
has_one :user_detail, class_name: 'CleanupProjectsWithMissingNamespace::UserDetail'
|
||||
has_one :namespace, -> { where(type: nil) },
|
||||
foreign_key: :owner_id, inverse_of: :owner, autosave: true,
|
||||
class_name: 'CleanupProjectsWithMissingNamespace::Namespace'
|
||||
|
||||
before_save :ensure_namespace_correct
|
||||
before_save :ensure_bio_is_assigned_to_user_details, if: :bio_changed?
|
||||
|
||||
enum project_view: { readme: 0, activity: 1, files: 2 }
|
||||
|
||||
def ensure_namespace_correct
|
||||
if namespace
|
||||
namespace.path = username if username_changed?
|
||||
namespace.name = name if name_changed?
|
||||
else
|
||||
build_namespace(path: username, name: name)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_bio_is_assigned_to_user_details
|
||||
return if Feature.disabled?(:migrate_bio_to_user_details, default_enabled: true)
|
||||
|
||||
user_detail.bio = bio.to_s[0...255]
|
||||
end
|
||||
|
||||
def user_detail
|
||||
super.presence || build_user_detail
|
||||
end
|
||||
|
||||
# Return (or create if necessary) the `lost-and-found` group
|
||||
def lost_and_found_group
|
||||
existing_lost_and_found_group || Group.create_unique_group(self, LOST_AND_FOUND_GROUP)
|
||||
end
|
||||
|
||||
def existing_lost_and_found_group
|
||||
# There should only be one Group for User Ghost starting with LOST_AND_FOUND_GROUP
|
||||
Group
|
||||
.joins('INNER JOIN members ON namespaces.id = members.source_id')
|
||||
.where('namespaces.type = ?', 'Group')
|
||||
.where('members.type = ?', 'GroupMember')
|
||||
.where('members.source_type = ?', 'Namespace')
|
||||
.where('members.user_id = ?', self.id)
|
||||
.where('members.requested_at IS NULL')
|
||||
.where('members.access_level = ?', ACCESS_LEVEL_OWNER)
|
||||
.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
|
||||
end
|
||||
|
||||
class << self
|
||||
# Return (or create if necessary) the ghost user
|
||||
def ghost
|
||||
email = 'ghost%s@example.com'
|
||||
|
||||
unique_internal(where(user_type: USER_TYPE_GHOST), 'ghost', email) do |u|
|
||||
u.bio = _('This is a "Ghost User", created to hold all issues authored by users that have since been deleted. This user cannot be removed.')
|
||||
u.name = 'Ghost User'
|
||||
end
|
||||
end
|
||||
|
||||
def unique_internal(scope, username, email_pattern, &block)
|
||||
scope.first || create_unique_internal(scope, username, email_pattern, &block)
|
||||
end
|
||||
|
||||
def create_unique_internal(scope, username, email_pattern, &creation_block)
|
||||
# Since we only want a single one of these in an instance, we use an
|
||||
# exclusive lease to ensure that this block is never run concurrently.
|
||||
lease_key = "user:unique_internal:#{username}"
|
||||
lease = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.minute.to_i)
|
||||
|
||||
until uuid = lease.try_obtain
|
||||
# Keep trying until we obtain the lease. To prevent hammering Redis too
|
||||
# much we'll wait for a bit between retries.
|
||||
sleep(1)
|
||||
end
|
||||
|
||||
# Recheck if the user is already present. One might have been
|
||||
# added between the time we last checked (first line of this method)
|
||||
# and the time we acquired the lock.
|
||||
existing_user = uncached { scope.first }
|
||||
return existing_user if existing_user.present?
|
||||
|
||||
uniquify = Uniquify.new
|
||||
|
||||
username = uniquify.string(username) { |s| User.find_by_username(s) }
|
||||
|
||||
email = uniquify.string(-> (n) { Kernel.sprintf(email_pattern, n) }) do |s|
|
||||
User.find_by_email(s)
|
||||
end
|
||||
|
||||
User.create!(
|
||||
username: username,
|
||||
email: email,
|
||||
user_type: USER_TYPE_GHOST,
|
||||
projects_limit: DEFAULT_PROJECTS_LIMIT,
|
||||
state: :active,
|
||||
&creation_block
|
||||
)
|
||||
ensure
|
||||
Gitlab::ExclusiveLease.cancel(lease_key, uuid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Namespace < ActiveRecord::Base
|
||||
self.table_name = 'namespaces'
|
||||
|
||||
belongs_to :owner, class_name: 'CleanupProjectsWithMissingNamespace::User'
|
||||
end
|
||||
|
||||
class Group < Namespace
|
||||
# Disable STI to allow us to manually set "type = 'Group'"
|
||||
# Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group"
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
def self.create_unique_group(user, group_name)
|
||||
# 'lost-and-found' may be already defined, find a unique one
|
||||
group_name = Uniquify.new.string(group_name) do |str|
|
||||
Group.where(parent_id: nil, name: str).exists?
|
||||
end
|
||||
|
||||
group = Group.create!(
|
||||
name: group_name,
|
||||
path: group_name,
|
||||
type: 'Group',
|
||||
description: 'Group to store orphaned projects',
|
||||
visibility_level: VISIBILITY_PRIVATE
|
||||
)
|
||||
|
||||
# No need to create a route for the lost-and-found group
|
||||
|
||||
GroupMember.add_user(group, user, ACCESS_LEVEL_OWNER)
|
||||
|
||||
group
|
||||
end
|
||||
end
|
||||
|
||||
class Member < ActiveRecord::Base
|
||||
self.table_name = 'members'
|
||||
end
|
||||
|
||||
class GroupMember < Member
|
||||
NOTIFICATION_SETTING_GLOBAL = 3
|
||||
|
||||
# Disable STI to allow us to manually set "type = 'GroupMember'"
|
||||
# Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::GroupMember"
|
||||
self.inheritance_column = :_type_disabled
|
||||
|
||||
def self.add_user(source, user, access_level)
|
||||
GroupMember.create!(
|
||||
type: 'GroupMember',
|
||||
source_id: source.id,
|
||||
user_id: user.id,
|
||||
source_type: 'Namespace',
|
||||
access_level: access_level,
|
||||
notification_level: NOTIFICATION_SETTING_GLOBAL
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
class Project < ActiveRecord::Base
|
||||
self.table_name = 'projects'
|
||||
|
||||
include ::EachBatch
|
||||
|
||||
def self.without_namespace
|
||||
where(
|
||||
'NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM namespaces
|
||||
WHERE projects.namespace_id = namespaces.id
|
||||
)'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def up
|
||||
# Reset the column information of all the models that update the database
|
||||
# to ensure the Active Record's knowledge of the table structure is current
|
||||
User.reset_column_information
|
||||
Namespace.reset_column_information
|
||||
Member.reset_column_information
|
||||
Project.reset_column_information
|
||||
|
||||
# Find or Create the ghost user
|
||||
ghost_user = User.ghost
|
||||
|
||||
# Find or Create the `lost-and-found`
|
||||
lost_and_found = ghost_user.lost_and_found_group
|
||||
|
||||
# With BATCH_SIZE=10K and projects.count=13.6M
|
||||
# ~1360 iterations will be run:
|
||||
# - each requires on average ~160ms for relation.without_namespace
|
||||
# - worst case scenario is that 66 of those batches will trigger an update (~200ms each)
|
||||
# In general, we expect less than 5% (=66/13.6M x 10K) to trigger an update
|
||||
# Expected total run time: ~235 seconds (== 220 seconds + 14 seconds)
|
||||
Project.each_batch(of: BATCH_SIZE) do |relation|
|
||||
relation.without_namespace.update_all <<~SQL
|
||||
namespace_id = #{lost_and_found.id},
|
||||
archived = TRUE,
|
||||
visibility_level = #{VISIBILITY_PRIVATE},
|
||||
|
||||
-- Names are expected to be unique inside their namespace
|
||||
-- (uniqueness validation on namespace_id, name)
|
||||
-- Attach the id to the name and path to make sure that they are unique
|
||||
name = name || '_' || id,
|
||||
path = path || '_' || id
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op: the original state for those projects was inconsistent
|
||||
# Also, the original namespace_id for each project is lost during the update
|
||||
end
|
||||
end
|
||||
# rubocop:enable Migration/PreventStrings
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ValidateProjectsForeignKeyToNamespaces < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
FK_NAME = 'fk_projects_namespace_id'
|
||||
|
||||
def up
|
||||
# Validate the FK added with 20200511080113_add_projects_foreign_key_to_namespaces.rb
|
||||
validate_foreign_key :projects, :namespace_id, name: FK_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op: No need to invalidate the foreign key
|
||||
# The inconsistent data are permanently fixed with the data migration
|
||||
# `20200511083541_cleanup_projects_with_missing_namespace.rb`
|
||||
# even if it is rolled back.
|
||||
# If there is an issue with the FK, we'll roll back the migration that adds the FK
|
||||
end
|
||||
end
|
|
@ -288,7 +288,6 @@ CREATE TABLE public.application_settings (
|
|||
geo_status_timeout integer DEFAULT 10,
|
||||
uuid character varying,
|
||||
polling_interval_multiplier numeric DEFAULT 1.0 NOT NULL,
|
||||
elasticsearch_experimental_indexer boolean,
|
||||
cached_markdown_version integer,
|
||||
check_namespace_plan boolean DEFAULT false NOT NULL,
|
||||
mirror_max_delay integer DEFAULT 300 NOT NULL,
|
||||
|
@ -10101,6 +10100,8 @@ CREATE INDEX index_namespaces_on_trial_ends_on ON public.namespaces USING btree
|
|||
|
||||
CREATE INDEX index_namespaces_on_type_partial ON public.namespaces USING btree (type) WHERE (type IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_non_requested_project_members_on_source_id_and_type ON public.members USING btree (source_id, source_type) WHERE ((requested_at IS NULL) AND ((type)::text = 'ProjectMember'::text));
|
||||
|
||||
CREATE UNIQUE INDEX index_note_diff_files_on_diff_note_id ON public.note_diff_files USING btree (diff_note_id);
|
||||
|
||||
CREATE INDEX index_notes_on_author_id_and_created_at_and_id ON public.notes USING btree (author_id, created_at, id);
|
||||
|
@ -11563,6 +11564,9 @@ ALTER TABLE ONLY public.personal_access_tokens
|
|||
ALTER TABLE ONLY public.project_settings
|
||||
ADD CONSTRAINT fk_project_settings_push_rule_id FOREIGN KEY (push_rule_id) REFERENCES public.push_rules(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY public.projects
|
||||
ADD CONSTRAINT fk_projects_namespace_id FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE ONLY public.protected_branch_merge_access_levels
|
||||
ADD CONSTRAINT fk_protected_branch_merge_access_levels_user_id FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -13818,6 +13822,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200424101920
|
||||
20200424135319
|
||||
20200427064130
|
||||
20200428134356
|
||||
20200429001827
|
||||
20200429002150
|
||||
20200429015603
|
||||
|
@ -13834,6 +13839,8 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200506154421
|
||||
20200507221434
|
||||
20200508091106
|
||||
20200511080113
|
||||
20200511083541
|
||||
20200511092246
|
||||
20200511092505
|
||||
20200511092714
|
||||
|
@ -13847,6 +13854,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200511145545
|
||||
20200511162057
|
||||
20200511162115
|
||||
20200511220023
|
||||
20200512085150
|
||||
20200512164334
|
||||
20200513160930
|
||||
|
@ -13858,5 +13866,6 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200514000009
|
||||
20200514000132
|
||||
20200514000340
|
||||
20200515155620
|
||||
\.
|
||||
|
||||
|
|
|
@ -17,6 +17,19 @@ developed and tested. We aim to be compatible with most external
|
|||
sudo -i
|
||||
```
|
||||
|
||||
1. Edit `/etc/gitlab/gitlab.rb` and add a **unique** ID for your node (arbitrary value):
|
||||
|
||||
```ruby
|
||||
# The unique identifier for the Geo node.
|
||||
gitlab_rails['geo_node_name'] = '<node_name_here>'
|
||||
```
|
||||
|
||||
1. Reconfigure the **primary** node for the change to take effect:
|
||||
|
||||
```shell
|
||||
gitlab-ctl reconfigure
|
||||
```
|
||||
|
||||
1. Execute the command below to define the node as **primary** node:
|
||||
|
||||
```shell
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# Instance-level CI/CD variables API
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14108) in GitLab 13.0
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14108) in GitLab 13.0
|
||||
> - It's deployed behind a feature flag, enabled by default.
|
||||
> - It's enabled on GitLab.com.
|
||||
> - It's recommended for production use.
|
||||
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-instance-level-cicd-variables-core-only). **(CORE ONLY)**
|
||||
|
||||
## List all instance variables
|
||||
|
||||
|
@ -137,3 +141,22 @@ DELETE /admin/ci/variables/:key
|
|||
```shell
|
||||
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/admin/ci/variables/VARIABLE_1"
|
||||
```
|
||||
|
||||
### Enable or disable instance-level CI/CD variables **(CORE ONLY)**
|
||||
|
||||
Instance-level CI/CD variables is under development but ready for production use.
|
||||
It is deployed behind a feature flag that is **enabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
|
||||
can opt to disable it for your instance.
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:ci_instance_level_variables)
|
||||
```
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:ci_instance_level_variables)
|
||||
```
|
||||
|
|
|
@ -23,6 +23,4 @@ type: index
|
|||
|
||||
## Securing your GitLab installation
|
||||
|
||||
To make sure your GitLab instance is safe and secure, please consider implementing
|
||||
[Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) to avoid
|
||||
malicious users creating accounts.
|
||||
Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/) to harden your GitLab instance and minimize the risk of unwanted user account creation.
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"copy-webpack-plugin": "^5.0.5",
|
||||
"core-js": "^3.6.4",
|
||||
"cropper": "^2.3.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"css-loader": "^2.1.1",
|
||||
"d3-scale": "^2.2.2",
|
||||
"d3-selection": "^1.2.0",
|
||||
"dateformat": "^3.0.3",
|
||||
|
|
|
@ -53,8 +53,6 @@ function rspec_simple_job() {
|
|||
|
||||
export NO_KNAPSACK="1"
|
||||
|
||||
scripts/gitaly-test-spawn
|
||||
|
||||
bin/rspec --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}
|
||||
}
|
||||
|
||||
|
@ -104,8 +102,6 @@ function rspec_paralellized_job() {
|
|||
fi
|
||||
fi
|
||||
|
||||
scripts/gitaly-test-spawn
|
||||
|
||||
mkdir -p tmp/memory_test
|
||||
|
||||
export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
|
||||
|
|
|
@ -14,7 +14,7 @@ describe ApplicationController do
|
|||
end
|
||||
|
||||
it 'redirects if the user is over their password expiry' do
|
||||
user.password_expires_at = Time.new(2002)
|
||||
user.password_expires_at = Time.zone.local(2002)
|
||||
|
||||
expect(user.ldap_user?).to be_falsey
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
|
@ -35,7 +35,7 @@ describe ApplicationController do
|
|||
end
|
||||
|
||||
it 'does not redirect if the user is over their password expiry but they are an ldap user' do
|
||||
user.password_expires_at = Time.new(2002)
|
||||
user.password_expires_at = Time.zone.local(2002)
|
||||
|
||||
allow(user).to receive(:ldap_user?).and_return(true)
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
|
@ -47,7 +47,7 @@ describe ApplicationController do
|
|||
it 'does not redirect if the user is over their password expiry but password authentication is disabled for the web interface' do
|
||||
stub_application_setting(password_authentication_enabled_for_web: false)
|
||||
stub_application_setting(password_authentication_enabled_for_git: false)
|
||||
user.password_expires_at = Time.new(2002)
|
||||
user.password_expires_at = Time.zone.local(2002)
|
||||
|
||||
allow(controller).to receive(:current_user).and_return(user)
|
||||
expect(controller).not_to receive(:redirect_to)
|
||||
|
|
|
@ -56,7 +56,7 @@ describe Groups::Settings::RepositoryController do
|
|||
'id' => be_a(Integer),
|
||||
'name' => deploy_token_params[:name],
|
||||
'username' => deploy_token_params[:username],
|
||||
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
|
||||
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
|
||||
'token' => be_a(String),
|
||||
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
|
||||
key, value = kv
|
||||
|
|
|
@ -48,8 +48,8 @@ describe Projects::GraphsController do
|
|||
|
||||
expect(assigns[:daily_coverage_options]).to eq(
|
||||
base_params: {
|
||||
start_date: Time.now.to_date - 90.days,
|
||||
end_date: Time.now.to_date,
|
||||
start_date: Time.current.to_date - 90.days,
|
||||
end_date: Time.current.to_date,
|
||||
ref_path: project.repository.expand_ref('master'),
|
||||
param_type: 'coverage'
|
||||
},
|
||||
|
|
|
@ -73,7 +73,7 @@ describe Projects::Settings::RepositoryController do
|
|||
'id' => be_a(Integer),
|
||||
'name' => deploy_token_params[:name],
|
||||
'username' => deploy_token_params[:username],
|
||||
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
|
||||
'expires_at' => Time.zone.parse(deploy_token_params[:expires_at]),
|
||||
'token' => be_a(String),
|
||||
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
|
||||
key, value = kv
|
||||
|
|
|
@ -64,7 +64,7 @@ describe 'Database schema' do
|
|||
open_project_tracker_data: %w[closed_status_id],
|
||||
project_group_links: %w[group_id],
|
||||
project_statistics: %w[namespace_id],
|
||||
projects: %w[creator_id namespace_id ci_id mirror_user_id],
|
||||
projects: %w[creator_id ci_id mirror_user_id],
|
||||
redirect_routes: %w[source_id],
|
||||
repository_languages: %w[programming_language_id],
|
||||
routes: %w[source_id],
|
||||
|
|
|
@ -9,8 +9,14 @@ import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dro
|
|||
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
|
||||
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
|
||||
import { BACKSPACE_KEY_CODE, DELETE_KEY_CODE } from '~/lib/utils/keycodes';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
describe('Filtered Search Manager', function() {
|
||||
jest.mock('~/lib/utils/url_utility', () => ({
|
||||
...jest.requireActual('~/lib/utils/url_utility'),
|
||||
visitUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('Filtered Search Manager', () => {
|
||||
let input;
|
||||
let manager;
|
||||
let tokensContainer;
|
||||
|
@ -68,17 +74,17 @@ describe('Filtered Search Manager', function() {
|
|||
</div>
|
||||
`);
|
||||
|
||||
spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
|
||||
jest.spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').mockImplementation();
|
||||
});
|
||||
|
||||
const initializeManager = () => {
|
||||
/* eslint-disable jasmine/no-unsafe-spy */
|
||||
spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
|
||||
spyOn(FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
|
||||
spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
|
||||
spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
|
||||
spyOn(FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
|
||||
/* eslint-enable jasmine/no-unsafe-spy */
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').mockImplementation();
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'tokenChange').mockImplementation();
|
||||
jest
|
||||
.spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset')
|
||||
.mockImplementation();
|
||||
jest.spyOn(gl.utils, 'getParameterByName').mockReturnValue(null);
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'unselectTokens');
|
||||
|
||||
input = document.querySelector('.filtered-search');
|
||||
tokensContainer = document.querySelector('.tokens-container');
|
||||
|
@ -92,22 +98,22 @@ describe('Filtered Search Manager', function() {
|
|||
|
||||
describe('class constructor', () => {
|
||||
const isLocalStorageAvailable = 'isLocalStorageAvailable';
|
||||
let RecentSearchesStoreSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(RecentSearchesService, 'isAvailable').and.returnValue(isLocalStorageAvailable);
|
||||
spyOn(RecentSearchesRoot.prototype, 'render');
|
||||
RecentSearchesStoreSpy = spyOnDependency(FilteredSearchManager, 'RecentSearchesStore');
|
||||
jest.spyOn(RecentSearchesService, 'isAvailable').mockReturnValue(isLocalStorageAvailable);
|
||||
jest.spyOn(RecentSearchesRoot.prototype, 'render').mockImplementation();
|
||||
});
|
||||
|
||||
it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => {
|
||||
manager = new FilteredSearchManager({ page });
|
||||
|
||||
expect(RecentSearchesService.isAvailable).toHaveBeenCalled();
|
||||
expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({
|
||||
isLocalStorageAvailable,
|
||||
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
|
||||
});
|
||||
expect(manager.recentSearchesStore.state).toEqual(
|
||||
expect.objectContaining({
|
||||
isLocalStorageAvailable,
|
||||
allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -117,10 +123,10 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => {
|
||||
spyOn(RecentSearchesService.prototype, 'fetch').and.callFake(() =>
|
||||
Promise.reject(new RecentSearchesServiceError()),
|
||||
);
|
||||
spyOn(window, 'Flash');
|
||||
jest
|
||||
.spyOn(RecentSearchesService.prototype, 'fetch')
|
||||
.mockImplementation(() => Promise.reject(new RecentSearchesServiceError()));
|
||||
jest.spyOn(window, 'Flash').mockImplementation();
|
||||
|
||||
manager.setup();
|
||||
|
||||
|
@ -130,7 +136,7 @@ describe('Filtered Search Manager', function() {
|
|||
|
||||
describe('searchState', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(FilteredSearchManager.prototype, 'search').and.callFake(() => {});
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'search').mockImplementation();
|
||||
initializeManager();
|
||||
});
|
||||
|
||||
|
@ -141,7 +147,7 @@ describe('Filtered Search Manager', function() {
|
|||
blur: () => {},
|
||||
},
|
||||
};
|
||||
spyOn(e.currentTarget, 'blur').and.callThrough();
|
||||
jest.spyOn(e.currentTarget, 'blur');
|
||||
manager.searchState(e);
|
||||
|
||||
expect(e.currentTarget.blur).toHaveBeenCalled();
|
||||
|
@ -187,7 +193,7 @@ describe('Filtered Search Manager', function() {
|
|||
it('should search with a single word', done => {
|
||||
input.value = 'searchTerm';
|
||||
|
||||
spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
|
||||
visitUrl.mockImplementation(url => {
|
||||
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
|
||||
done();
|
||||
});
|
||||
|
@ -198,7 +204,7 @@ describe('Filtered Search Manager', function() {
|
|||
it('should search with multiple words', done => {
|
||||
input.value = 'awesome search terms';
|
||||
|
||||
spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
|
||||
visitUrl.mockImplementation(url => {
|
||||
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
|
||||
done();
|
||||
});
|
||||
|
@ -209,7 +215,7 @@ describe('Filtered Search Manager', function() {
|
|||
it('should search with special characters', done => {
|
||||
input.value = '~!@#$%^&*()_+{}:<>,.?/';
|
||||
|
||||
spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
|
||||
visitUrl.mockImplementation(url => {
|
||||
expect(url).toEqual(
|
||||
`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`,
|
||||
);
|
||||
|
@ -225,7 +231,7 @@ describe('Filtered Search Manager', function() {
|
|||
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
|
||||
`);
|
||||
|
||||
spyOnDependency(FilteredSearchManager, 'visitUrl').and.callFake(url => {
|
||||
visitUrl.mockImplementation(url => {
|
||||
expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
|
||||
done();
|
||||
});
|
||||
|
@ -277,7 +283,7 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('removes last token', () => {
|
||||
spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
|
||||
dispatchBackspaceEvent(input, 'keyup');
|
||||
dispatchBackspaceEvent(input, 'keyup');
|
||||
|
||||
|
@ -285,7 +291,7 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('sets the input', () => {
|
||||
spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
|
||||
dispatchDeleteEvent(input, 'keyup');
|
||||
dispatchDeleteEvent(input, 'keyup');
|
||||
|
||||
|
@ -295,8 +301,8 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('does not remove token or change input when there is existing input', () => {
|
||||
spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
|
||||
spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
|
||||
|
||||
input.value = 'text';
|
||||
dispatchDeleteEvent(input, 'keyup');
|
||||
|
@ -307,8 +313,8 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('does not remove previous token on single backspace press', () => {
|
||||
spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
|
||||
spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial');
|
||||
|
||||
input.value = 't';
|
||||
dispatchDeleteEvent(input, 'keyup');
|
||||
|
@ -322,7 +328,7 @@ describe('Filtered Search Manager', function() {
|
|||
describe('checkForAltOrCtrlBackspace', () => {
|
||||
beforeEach(() => {
|
||||
initializeManager();
|
||||
spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial');
|
||||
});
|
||||
|
||||
describe('tokens and no input', () => {
|
||||
|
@ -384,7 +390,7 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
it('removes all tokens and input', () => {
|
||||
spyOn(FilteredSearchManager.prototype, 'clearSearch').and.callThrough();
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'clearSearch');
|
||||
dispatchMetaBackspaceEvent(input, 'keydown');
|
||||
|
||||
expect(manager.clearSearch).toHaveBeenCalled();
|
||||
|
@ -410,7 +416,7 @@ describe('Filtered Search Manager', function() {
|
|||
|
||||
describe('unselected token', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough();
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'removeSelectedToken');
|
||||
|
||||
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
|
||||
FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', 'none'),
|
||||
|
@ -481,9 +487,9 @@ describe('Filtered Search Manager', function() {
|
|||
|
||||
describe('removeSelectedToken', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough();
|
||||
spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough();
|
||||
spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough();
|
||||
jest.spyOn(FilteredSearchVisualTokens, 'removeSelectedToken');
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder');
|
||||
jest.spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton');
|
||||
initializeManager();
|
||||
});
|
||||
|
||||
|
@ -554,8 +560,9 @@ describe('Filtered Search Manager', function() {
|
|||
});
|
||||
|
||||
describe('getAllParams', () => {
|
||||
let paramsArr;
|
||||
beforeEach(() => {
|
||||
this.paramsArr = ['key=value', 'otherkey=othervalue'];
|
||||
paramsArr = ['key=value', 'otherkey=othervalue'];
|
||||
|
||||
initializeManager();
|
||||
});
|
||||
|
@ -563,18 +570,18 @@ describe('Filtered Search Manager', function() {
|
|||
it('correctly modifies params when custom modifier is passed', () => {
|
||||
const modifedParams = manager.getAllParams.call(
|
||||
{
|
||||
modifyUrlParams: paramsArr => paramsArr.reverse(),
|
||||
modifyUrlParams: params => params.reverse(),
|
||||
},
|
||||
[].concat(this.paramsArr),
|
||||
[].concat(paramsArr),
|
||||
);
|
||||
|
||||
expect(modifedParams[0]).toBe(this.paramsArr[1]);
|
||||
expect(modifedParams[0]).toBe(paramsArr[1]);
|
||||
});
|
||||
|
||||
it('does not modify params when no custom modifier is passed', () => {
|
||||
const modifedParams = manager.getAllParams.call({}, this.paramsArr);
|
||||
const modifedParams = manager.getAllParams.call({}, paramsArr);
|
||||
|
||||
expect(modifedParams[1]).toBe(this.paramsArr[1]);
|
||||
expect(modifedParams[1]).toBe(paramsArr[1]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
|
||||
|
||||
jest.mock('vue');
|
||||
|
||||
describe('RecentSearchesRoot', () => {
|
||||
describe('render', () => {
|
||||
let recentSearchesRoot;
|
||||
let data;
|
||||
let template;
|
||||
let VueSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
recentSearchesRoot = {
|
||||
|
@ -14,7 +16,7 @@ describe('RecentSearchesRoot', () => {
|
|||
},
|
||||
};
|
||||
|
||||
VueSpy = spyOnDependency(RecentSearchesRoot, 'Vue').and.callFake(options => {
|
||||
Vue.mockImplementation(options => {
|
||||
({ data, template } = options);
|
||||
});
|
||||
|
||||
|
@ -22,7 +24,7 @@ describe('RecentSearchesRoot', () => {
|
|||
});
|
||||
|
||||
it('should instantiate Vue', () => {
|
||||
expect(VueSpy).toHaveBeenCalled();
|
||||
expect(Vue).toHaveBeenCalled();
|
||||
expect(data()).toBe(recentSearchesRoot.store.state);
|
||||
expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Vue from 'vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import appComponent from '~/frequent_items/components/app.vue';
|
||||
import eventHub from '~/frequent_items/event_hub';
|
||||
|
@ -8,6 +8,10 @@ import store from '~/frequent_items/store';
|
|||
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
|
||||
import { getTopFrequentItems } from '~/frequent_items/utils';
|
||||
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
useLocalStorageSpy();
|
||||
|
||||
let session;
|
||||
const createComponentWithStore = (namespace = 'projects') => {
|
||||
|
@ -42,7 +46,7 @@ describe('Frequent Items App Component', () => {
|
|||
describe('methods', () => {
|
||||
describe('dropdownOpenHandler', () => {
|
||||
it('should fetch frequent items when no search has been previously made on desktop', () => {
|
||||
spyOn(vm, 'fetchFrequentItems');
|
||||
jest.spyOn(vm, 'fetchFrequentItems').mockImplementation(() => {});
|
||||
|
||||
vm.dropdownOpenHandler();
|
||||
|
||||
|
@ -56,11 +60,11 @@ describe('Frequent Items App Component', () => {
|
|||
beforeEach(() => {
|
||||
storage = {};
|
||||
|
||||
spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
|
||||
localStorage.setItem.mockImplementation((storageKey, value) => {
|
||||
storage[storageKey] = value;
|
||||
});
|
||||
|
||||
spyOn(window.localStorage, 'getItem').and.callFake(storageKey => {
|
||||
localStorage.getItem.mockImplementation(storageKey => {
|
||||
if (storage[storageKey]) {
|
||||
return storage[storageKey];
|
||||
}
|
||||
|
@ -156,12 +160,12 @@ describe('Frequent Items App Component', () => {
|
|||
|
||||
describe('created', () => {
|
||||
it('should bind event listeners on eventHub', done => {
|
||||
spyOn(eventHub, '$on');
|
||||
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
|
||||
|
||||
createComponentWithStore().$mount();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
|
||||
expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', expect.any(Function));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -169,13 +173,13 @@ describe('Frequent Items App Component', () => {
|
|||
|
||||
describe('beforeDestroy', () => {
|
||||
it('should unbind event listeners on eventHub', done => {
|
||||
spyOn(eventHub, '$off');
|
||||
jest.spyOn(eventHub, '$off').mockImplementation(() => {});
|
||||
|
||||
vm.$mount();
|
||||
vm.$destroy();
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
|
||||
expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', expect.any(Function));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -211,9 +215,7 @@ describe('Frequent Items App Component', () => {
|
|||
|
||||
it('should render frequent projects list', done => {
|
||||
const expectedResult = getTopFrequentItems(mockFrequentProjects);
|
||||
spyOn(window.localStorage, 'getItem').and.callFake(() =>
|
||||
JSON.stringify(mockFrequentProjects),
|
||||
);
|
||||
localStorage.getItem.mockImplementation(() => JSON.stringify(mockFrequentProjects));
|
||||
|
||||
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
|
||||
|
||||
|
@ -236,15 +238,7 @@ describe('Frequent Items App Component', () => {
|
|||
.then(() => {
|
||||
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
|
||||
})
|
||||
|
||||
// This test waits for multiple ticks in order to allow the responses to
|
||||
// propagate through each interceptor installed on the Axios instance.
|
||||
// This shouldn't be necessary; this test should be refactored to avoid this.
|
||||
// https://gitlab.com/gitlab-org/gitlab/issues/32479
|
||||
.then(vm.$nextTick)
|
||||
.then(vm.$nextTick)
|
||||
.then(vm.$nextTick)
|
||||
|
||||
.then(waitForPromises)
|
||||
.then(() => {
|
||||
expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
|
||||
mockSearchedProjects.data.length,
|
|
@ -1,5 +1,94 @@
|
|||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
|
||||
export const currentSession = {
|
||||
groups: {
|
||||
username: 'root',
|
||||
storageKey: 'root/frequent-groups',
|
||||
apiVersion: 'v4',
|
||||
group: {
|
||||
id: 1,
|
||||
name: 'dummy-group',
|
||||
full_name: 'dummy-parent-group',
|
||||
webUrl: `${TEST_HOST}/dummy-group`,
|
||||
avatarUrl: null,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
username: 'root',
|
||||
storageKey: 'root/frequent-projects',
|
||||
apiVersion: 'v4',
|
||||
project: {
|
||||
id: 1,
|
||||
name: 'dummy-project',
|
||||
namespace: 'SampleGroup / Dummy-Project',
|
||||
webUrl: `${TEST_HOST}/samplegroup/dummy-project`,
|
||||
avatarUrl: null,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockNamespace = 'projects';
|
||||
|
||||
export const mockStorageKey = 'test-user/frequent-projects';
|
||||
|
||||
export const mockGroup = {
|
||||
id: 1,
|
||||
name: 'Sub451',
|
||||
namespace: 'Commit451 / Sub451',
|
||||
webUrl: `${TEST_HOST}/Commit451/Sub451`,
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
export const mockRawGroup = {
|
||||
id: 1,
|
||||
name: 'Sub451',
|
||||
full_name: 'Commit451 / Sub451',
|
||||
web_url: `${TEST_HOST}/Commit451/Sub451`,
|
||||
avatar_url: null,
|
||||
};
|
||||
|
||||
export const mockFrequentGroups = [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Subgroup451',
|
||||
full_name: 'Commit451 / Subgroup451',
|
||||
webUrl: '/Commit451/Subgroup451',
|
||||
avatarUrl: null,
|
||||
frequency: 7,
|
||||
lastAccessedOn: 1497979281815,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Commit451',
|
||||
full_name: 'Commit451',
|
||||
webUrl: '/Commit451',
|
||||
avatarUrl: null,
|
||||
frequency: 3,
|
||||
lastAccessedOn: 1497979281815,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSearchedGroups = [mockRawGroup];
|
||||
export const mockProcessedSearchedGroups = [mockGroup];
|
||||
|
||||
export const mockProject = {
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
namespace: 'gitlab-org / gitlab-ce',
|
||||
webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
export const mockRawProject = {
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
name_with_namespace: 'gitlab-org / gitlab-ce',
|
||||
web_url: `${TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatar_url: null,
|
||||
};
|
||||
|
||||
export const mockFrequentProjects = [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -48,10 +137,34 @@ export const mockFrequentProjects = [
|
|||
},
|
||||
];
|
||||
|
||||
export const mockProject = {
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
namespace: 'gitlab-org / gitlab-ce',
|
||||
webUrl: `${TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatarUrl: null,
|
||||
};
|
||||
export const mockSearchedProjects = { data: [mockRawProject] };
|
||||
export const mockProcessedSearchedProjects = [mockProject];
|
||||
|
||||
export const unsortedFrequentItems = [
|
||||
{ id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
|
||||
{ id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
|
||||
{ id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
|
||||
{ id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
|
||||
{ id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
|
||||
{ id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
|
||||
{ id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
|
||||
{ id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
|
||||
{ id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
|
||||
];
|
||||
|
||||
/**
|
||||
* This const has a specific order which tests authenticity
|
||||
* of `getTopFrequentItems` method so
|
||||
* DO NOT change order of items in this const.
|
||||
*/
|
||||
export const sortedFrequentItems = [
|
||||
{ id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
|
||||
{ id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
|
||||
{ id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
|
||||
{ id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
|
||||
{ id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
|
||||
{ id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
|
||||
{ id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
|
||||
{ id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
|
||||
{ id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
|
||||
];
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import testAction from 'spec/helpers/vuex_action_helper';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import AccessorUtilities from '~/lib/utils/accessor';
|
||||
|
@ -109,7 +109,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
|
|||
});
|
||||
|
||||
it('should dispatch `receiveFrequentItemsError`', done => {
|
||||
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(false);
|
||||
jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(false);
|
||||
mockedState.namespace = mockNamespace;
|
||||
mockedState.storageKey = mockStorageKey;
|
||||
|
|
@ -11,25 +11,25 @@ import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_
|
|||
describe('Frequent Items utils spec', () => {
|
||||
describe('isMobile', () => {
|
||||
it('returns true when the screen is medium ', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('md');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
|
||||
|
||||
expect(isMobile()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the screen is small ', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm');
|
||||
|
||||
expect(isMobile()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when the screen is extra-small ', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs');
|
||||
|
||||
expect(isMobile()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when the screen is larger than medium ', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg');
|
||||
|
||||
expect(isMobile()).toBe(false);
|
||||
});
|
||||
|
@ -43,21 +43,21 @@ describe('Frequent Items utils spec', () => {
|
|||
});
|
||||
|
||||
it('returns correct amount of items for mobile', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('md');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md');
|
||||
const result = getTopFrequentItems(unsortedFrequentItems);
|
||||
|
||||
expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_MOBILE);
|
||||
});
|
||||
|
||||
it('returns correct amount of items for desktop', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('xl');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
|
||||
const result = getTopFrequentItems(unsortedFrequentItems);
|
||||
|
||||
expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
|
||||
});
|
||||
|
||||
it('sorts frequent items in order of frequency and lastAccessedOn', () => {
|
||||
spyOn(bp, 'getBreakpointSize').and.returnValue('xl');
|
||||
jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xl');
|
||||
const result = getTopFrequentItems(unsortedFrequentItems);
|
||||
const expectedResult = sortedFrequentItems.slice(0, FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
|
||||
|
|
@ -23,11 +23,12 @@ Did you run bin/rake frontend:fixtures?`,
|
|||
export const getJSONFixture = relativePath => JSON.parse(getFixture(relativePath));
|
||||
|
||||
export const resetHTMLFixture = () => {
|
||||
document.body.textContent = '';
|
||||
document.head.innerHTML = '';
|
||||
document.body.innerHTML = '';
|
||||
};
|
||||
|
||||
export const setHTMLFixture = (htmlContent, resetHook = afterEach) => {
|
||||
document.body.outerHTML = htmlContent;
|
||||
document.body.innerHTML = htmlContent;
|
||||
resetHook(resetHTMLFixture);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,37 +1,44 @@
|
|||
import csrf from '~/lib/utils/csrf';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
describe('csrf', () => {
|
||||
let testContext;
|
||||
|
||||
describe('csrf', function() {
|
||||
beforeEach(() => {
|
||||
this.tokenKey = 'X-CSRF-Token';
|
||||
this.token =
|
||||
testContext = {};
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
testContext.tokenKey = 'X-CSRF-Token';
|
||||
testContext.token =
|
||||
'pH1cvjnP9grx2oKlhWEDvUZnJ8x2eXsIs1qzyHkF3DugSG5yTxR76CWeEZRhML2D1IeVB7NEW0t5l/axE4iJpQ==';
|
||||
});
|
||||
|
||||
it('returns the correct headerKey', () => {
|
||||
expect(csrf.headerKey).toBe(this.tokenKey);
|
||||
expect(csrf.headerKey).toBe(testContext.tokenKey);
|
||||
});
|
||||
|
||||
describe('when csrf token is in the DOM', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<meta name="csrf-token" content="${this.token}">
|
||||
setHTMLFixture(`
|
||||
<meta name="csrf-token" content="${testContext.token}">
|
||||
`);
|
||||
|
||||
csrf.init();
|
||||
});
|
||||
|
||||
it('returns the csrf token', () => {
|
||||
expect(csrf.token).toBe(this.token);
|
||||
expect(csrf.token).toBe(testContext.token);
|
||||
});
|
||||
|
||||
it('returns the csrf headers object', () => {
|
||||
expect(csrf.headers[this.tokenKey]).toBe(this.token);
|
||||
expect(csrf.headers[testContext.tokenKey]).toBe(testContext.token);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when csrf token is not in the DOM', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
setHTMLFixture(`
|
||||
<meta name="some-other-token">
|
||||
`);
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
import findAndFollowLink from '~/lib/utils/navigation_utility';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
||||
jest.mock('~/lib/utils/url_utility');
|
||||
|
||||
describe('findAndFollowLink', () => {
|
||||
it('visits a link when the selector exists', () => {
|
||||
const href = '/some/path';
|
||||
const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
|
||||
|
||||
setFixtures(`<a class="my-shortcut" href="${href}">link</a>`);
|
||||
|
||||
|
@ -13,8 +15,6 @@ describe('findAndFollowLink', () => {
|
|||
});
|
||||
|
||||
it('does not throw an exception when the selector does not exist', () => {
|
||||
const visitUrl = spyOnDependency(findAndFollowLink, 'visitUrl');
|
||||
|
||||
// this should not throw an exception
|
||||
findAndFollowLink('.this-selector-does-not-exist');
|
||||
|
|
@ -1,34 +1,10 @@
|
|||
/* eslint-disable jasmine/no-unsafe-spy */
|
||||
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import { successCodes } from '~/lib/utils/http_status';
|
||||
|
||||
const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
|
||||
const timer = () => {
|
||||
setTimeout(() => {
|
||||
if (service.fetch.calls.count() === waitForCount) {
|
||||
successCallback();
|
||||
} else {
|
||||
timer();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
|
||||
timer();
|
||||
};
|
||||
|
||||
function mockServiceCall(service, response, shouldFail = false) {
|
||||
const action = shouldFail ? Promise.reject : Promise.resolve;
|
||||
const responseObject = response;
|
||||
|
||||
if (!responseObject.headers) responseObject.headers = {};
|
||||
|
||||
service.fetch.and.callFake(action.bind(Promise, responseObject));
|
||||
}
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
describe('Poll', () => {
|
||||
const service = jasmine.createSpyObj('service', ['fetch']);
|
||||
const callbacks = jasmine.createSpyObj('callbacks', ['success', 'error', 'notification']);
|
||||
let callbacks;
|
||||
let service;
|
||||
|
||||
function setup() {
|
||||
return new Poll({
|
||||
|
@ -40,18 +16,45 @@ describe('Poll', () => {
|
|||
}).makeRequest();
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
callbacks.success.calls.reset();
|
||||
callbacks.error.calls.reset();
|
||||
callbacks.notification.calls.reset();
|
||||
service.fetch.calls.reset();
|
||||
const mockServiceCall = (response, shouldFail = false) => {
|
||||
const value = {
|
||||
...response,
|
||||
header: response.header || {},
|
||||
};
|
||||
|
||||
if (shouldFail) {
|
||||
service.fetch.mockRejectedValue(value);
|
||||
} else {
|
||||
service.fetch.mockResolvedValue(value);
|
||||
}
|
||||
};
|
||||
|
||||
const waitForAllCallsToFinish = (waitForCount, successCallback) => {
|
||||
if (!waitForCount) {
|
||||
return Promise.resolve().then(successCallback());
|
||||
}
|
||||
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
return waitForPromises().then(() => waitForAllCallsToFinish(waitForCount - 1, successCallback));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
service = {
|
||||
fetch: jest.fn(),
|
||||
};
|
||||
callbacks = {
|
||||
success: jest.fn(),
|
||||
error: jest.fn(),
|
||||
notification: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('calls the success callback when no header for interval is provided', done => {
|
||||
mockServiceCall(service, { status: 200 });
|
||||
mockServiceCall({ status: 200 });
|
||||
setup();
|
||||
|
||||
waitForAllCallsToFinish(service, 1, () => {
|
||||
waitForAllCallsToFinish(1, () => {
|
||||
expect(callbacks.success).toHaveBeenCalled();
|
||||
expect(callbacks.error).not.toHaveBeenCalled();
|
||||
|
||||
|
@ -60,10 +63,10 @@ describe('Poll', () => {
|
|||
});
|
||||
|
||||
it('calls the error callback when the http request returns an error', done => {
|
||||
mockServiceCall(service, { status: 500 }, true);
|
||||
mockServiceCall({ status: 500 }, true);
|
||||
setup();
|
||||
|
||||
waitForAllCallsToFinish(service, 1, () => {
|
||||
waitForAllCallsToFinish(1, () => {
|
||||
expect(callbacks.success).not.toHaveBeenCalled();
|
||||
expect(callbacks.error).toHaveBeenCalled();
|
||||
|
||||
|
@ -72,10 +75,10 @@ describe('Poll', () => {
|
|||
});
|
||||
|
||||
it('skips the error callback when request is aborted', done => {
|
||||
mockServiceCall(service, { status: 0 }, true);
|
||||
mockServiceCall({ status: 0 }, true);
|
||||
setup();
|
||||
|
||||
waitForAllCallsToFinish(service, 1, () => {
|
||||
waitForAllCallsToFinish(1, () => {
|
||||
expect(callbacks.success).not.toHaveBeenCalled();
|
||||
expect(callbacks.error).not.toHaveBeenCalled();
|
||||
expect(callbacks.notification).toHaveBeenCalled();
|
||||
|
@ -85,7 +88,7 @@ describe('Poll', () => {
|
|||
});
|
||||
|
||||
it('should call the success callback when the interval header is -1', done => {
|
||||
mockServiceCall(service, { status: 200, headers: { 'poll-interval': -1 } });
|
||||
mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
|
||||
setup()
|
||||
.then(() => {
|
||||
expect(callbacks.success).toHaveBeenCalled();
|
||||
|
@ -99,7 +102,7 @@ describe('Poll', () => {
|
|||
describe('for 2xx status code', () => {
|
||||
successCodes.forEach(httpCode => {
|
||||
it(`starts polling when http status is ${httpCode} and interval header is provided`, done => {
|
||||
mockServiceCall(service, { status: httpCode, headers: { 'poll-interval': 1 } });
|
||||
mockServiceCall({ status: httpCode, headers: { 'poll-interval': 1 } });
|
||||
|
||||
const Polling = new Poll({
|
||||
resource: service,
|
||||
|
@ -111,10 +114,10 @@ describe('Poll', () => {
|
|||
|
||||
Polling.makeRequest();
|
||||
|
||||
waitForAllCallsToFinish(service, 2, () => {
|
||||
waitForAllCallsToFinish(2, () => {
|
||||
Polling.stop();
|
||||
|
||||
expect(service.fetch.calls.count()).toEqual(2);
|
||||
expect(service.fetch.mock.calls).toHaveLength(2);
|
||||
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
|
||||
expect(callbacks.success).toHaveBeenCalled();
|
||||
expect(callbacks.error).not.toHaveBeenCalled();
|
||||
|
@ -127,7 +130,7 @@ describe('Poll', () => {
|
|||
|
||||
describe('stop', () => {
|
||||
it('stops polling when method is called', done => {
|
||||
mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
|
||||
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
|
||||
|
||||
const Polling = new Poll({
|
||||
resource: service,
|
||||
|
@ -139,12 +142,12 @@ describe('Poll', () => {
|
|||
errorCallback: callbacks.error,
|
||||
});
|
||||
|
||||
spyOn(Polling, 'stop').and.callThrough();
|
||||
jest.spyOn(Polling, 'stop');
|
||||
|
||||
Polling.makeRequest();
|
||||
|
||||
waitForAllCallsToFinish(service, 1, () => {
|
||||
expect(service.fetch.calls.count()).toEqual(1);
|
||||
waitForAllCallsToFinish(1, () => {
|
||||
expect(service.fetch.mock.calls).toHaveLength(1);
|
||||
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
|
||||
expect(Polling.stop).toHaveBeenCalled();
|
||||
|
||||
|
@ -155,8 +158,7 @@ describe('Poll', () => {
|
|||
|
||||
describe('enable', () => {
|
||||
it('should enable polling upon a response', done => {
|
||||
jasmine.clock().install();
|
||||
|
||||
mockServiceCall({ status: 200 });
|
||||
const Polling = new Poll({
|
||||
resource: service,
|
||||
method: 'fetch',
|
||||
|
@ -169,13 +171,10 @@ describe('Poll', () => {
|
|||
response: { status: 200, headers: { 'poll-interval': 1 } },
|
||||
});
|
||||
|
||||
jasmine.clock().tick(1);
|
||||
jasmine.clock().uninstall();
|
||||
|
||||
waitForAllCallsToFinish(service, 1, () => {
|
||||
waitForAllCallsToFinish(1, () => {
|
||||
Polling.stop();
|
||||
|
||||
expect(service.fetch.calls.count()).toEqual(1);
|
||||
expect(service.fetch.mock.calls).toHaveLength(1);
|
||||
expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
|
||||
expect(Polling.options.data).toEqual({ page: 4 });
|
||||
done();
|
||||
|
@ -185,7 +184,7 @@ describe('Poll', () => {
|
|||
|
||||
describe('restart', () => {
|
||||
it('should restart polling when its called', done => {
|
||||
mockServiceCall(service, { status: 200, headers: { 'poll-interval': 1 } });
|
||||
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
|
||||
|
||||
const Polling = new Poll({
|
||||
resource: service,
|
||||
|
@ -193,23 +192,27 @@ describe('Poll', () => {
|
|||
data: { page: 1 },
|
||||
successCallback: () => {
|
||||
Polling.stop();
|
||||
|
||||
// Let's pretend that we asynchronously restart this.
|
||||
// setTimeout is mocked but this will actually get triggered
|
||||
// in waitForAllCalssToFinish.
|
||||
setTimeout(() => {
|
||||
Polling.restart({ data: { page: 4 } });
|
||||
}, 0);
|
||||
}, 1);
|
||||
},
|
||||
errorCallback: callbacks.error,
|
||||
});
|
||||
|
||||
spyOn(Polling, 'stop').and.callThrough();
|
||||
spyOn(Polling, 'enable').and.callThrough();
|
||||
spyOn(Polling, 'restart').and.callThrough();
|
||||
jest.spyOn(Polling, 'stop');
|
||||
jest.spyOn(Polling, 'enable');
|
||||
jest.spyOn(Polling, 'restart');
|
||||
|
||||
Polling.makeRequest();
|
||||
|
||||
waitForAllCallsToFinish(service, 2, () => {
|
||||
waitForAllCallsToFinish(2, () => {
|
||||
Polling.stop();
|
||||
|
||||
expect(service.fetch.calls.count()).toEqual(2);
|
||||
expect(service.fetch.mock.calls).toHaveLength(2);
|
||||
expect(service.fetch).toHaveBeenCalledWith({ page: 4 });
|
||||
expect(Polling.stop).toHaveBeenCalled();
|
||||
expect(Polling.enable).toHaveBeenCalled();
|
|
@ -1,20 +1,32 @@
|
|||
import { isSticky } from '~/lib/utils/sticky';
|
||||
import { setHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
const TEST_OFFSET_TOP = 500;
|
||||
|
||||
describe('sticky', () => {
|
||||
let el;
|
||||
let offsetTop;
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML += `
|
||||
setHTMLFixture(
|
||||
`
|
||||
<div class="parent">
|
||||
<div id="js-sticky"></div>
|
||||
</div>
|
||||
`;
|
||||
`,
|
||||
);
|
||||
|
||||
offsetTop = TEST_OFFSET_TOP;
|
||||
el = document.getElementById('js-sticky');
|
||||
Object.defineProperty(el, 'offsetTop', {
|
||||
get() {
|
||||
return offsetTop;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
el.parentNode.remove();
|
||||
el = null;
|
||||
});
|
||||
|
||||
describe('when stuck', () => {
|
||||
|
@ -40,14 +52,13 @@ describe('sticky', () => {
|
|||
|
||||
describe('when not stuck', () => {
|
||||
it('removes is-stuck class', () => {
|
||||
spyOn(el.classList, 'remove').and.callThrough();
|
||||
jest.spyOn(el.classList, 'remove');
|
||||
|
||||
isSticky(el, 0, el.offsetTop);
|
||||
isSticky(el, 0, 0);
|
||||
|
||||
expect(el.classList.remove).toHaveBeenCalledWith('is-stuck');
|
||||
|
||||
expect(el.classList.contains('is-stuck')).toBeFalsy();
|
||||
expect(el.classList.contains('is-stuck')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not add is-stuck class', () => {
|
|
@ -9,12 +9,7 @@ import CommentForm from '~/notes/components/comment_form.vue';
|
|||
import * as constants from '~/notes/constants';
|
||||
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
|
||||
import { keyboardDownEvent } from '../../issue_show/helpers';
|
||||
import {
|
||||
loggedOutnoteableData,
|
||||
notesDataMock,
|
||||
userDataMock,
|
||||
noteableDataMock,
|
||||
} from '../../notes/mock_data';
|
||||
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
|
||||
|
||||
jest.mock('autosize');
|
||||
jest.mock('~/commons/nav/user_merge_requests');
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { discussionMock } from '../../notes/mock_data';
|
||||
import { discussionMock } from '../mock_data';
|
||||
import DiscussionActions from '~/notes/components/discussion_actions.vue';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
|
||||
|
|
|
@ -7,7 +7,7 @@ import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'
|
|||
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
||||
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
|
||||
import createStore from '~/notes/stores';
|
||||
import { noteableDataMock, discussionMock, notesDataMock } from '../../notes/mock_data';
|
||||
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
|
||||
|
||||
describe('DiscussionNotes', () => {
|
||||
let wrapper;
|
||||
|
|
|
@ -10,7 +10,7 @@ import createStore from '~/notes/stores';
|
|||
import * as constants from '~/notes/constants';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
|
||||
import * as mockData from '../../notes/mock_data';
|
||||
import * as mockData from '../mock_data';
|
||||
import * as urlUtility from '~/lib/utils/url_utility';
|
||||
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
|
||||
|
||||
|
|
|
@ -17,6 +17,12 @@ describe('RelatedMergeRequests', () => {
|
|||
beforeEach(done => {
|
||||
loadFixtures(FIXTURE_PATH);
|
||||
mockData = getJSONFixture(FIXTURE_PATH);
|
||||
|
||||
// put the fixture in DOM as the component expects
|
||||
document.body.innerHTML = `<div id="js-issuable-app-initial-data">${JSON.stringify(
|
||||
mockData,
|
||||
)}</div>`;
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
|
||||
|
||||
|
@ -30,7 +36,7 @@ describe('RelatedMergeRequests', () => {
|
|||
},
|
||||
});
|
||||
|
||||
setTimeout(done);
|
||||
setImmediate(done);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
|
@ -1,19 +1,20 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'spec/helpers/vuex_action_helper';
|
||||
import createFlash from '~/flash';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as types from '~/related_merge_requests/store/mutation_types';
|
||||
import actionsModule, * as actions from '~/related_merge_requests/store/actions';
|
||||
import * as actions from '~/related_merge_requests/store/actions';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
describe('RelatedMergeRequest store actions', () => {
|
||||
let state;
|
||||
let flashSpy;
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
state = {
|
||||
apiEndpoint: '/api/related_merge_requests',
|
||||
};
|
||||
flashSpy = spyOnDependency(actionsModule, 'createFlash');
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
|
@ -98,8 +99,8 @@ describe('RelatedMergeRequest store actions', () => {
|
|||
[],
|
||||
[{ type: 'requestData' }, { type: 'receiveDataError' }],
|
||||
() => {
|
||||
expect(flashSpy).toHaveBeenCalledTimes(1);
|
||||
expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
|
||||
expect(createFlash).toHaveBeenCalledTimes(1);
|
||||
expect(createFlash).toHaveBeenCalledWith(expect.stringMatching('Something went wrong'));
|
||||
|
||||
done();
|
||||
},
|
|
@ -1,168 +0,0 @@
|
|||
export const currentSession = {
|
||||
groups: {
|
||||
username: 'root',
|
||||
storageKey: 'root/frequent-groups',
|
||||
apiVersion: 'v4',
|
||||
group: {
|
||||
id: 1,
|
||||
name: 'dummy-group',
|
||||
full_name: 'dummy-parent-group',
|
||||
webUrl: `${gl.TEST_HOST}/dummy-group`,
|
||||
avatarUrl: null,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
},
|
||||
projects: {
|
||||
username: 'root',
|
||||
storageKey: 'root/frequent-projects',
|
||||
apiVersion: 'v4',
|
||||
project: {
|
||||
id: 1,
|
||||
name: 'dummy-project',
|
||||
namespace: 'SampleGroup / Dummy-Project',
|
||||
webUrl: `${gl.TEST_HOST}/samplegroup/dummy-project`,
|
||||
avatarUrl: null,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockNamespace = 'projects';
|
||||
|
||||
export const mockStorageKey = 'test-user/frequent-projects';
|
||||
|
||||
export const mockGroup = {
|
||||
id: 1,
|
||||
name: 'Sub451',
|
||||
namespace: 'Commit451 / Sub451',
|
||||
webUrl: `${gl.TEST_HOST}/Commit451/Sub451`,
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
export const mockRawGroup = {
|
||||
id: 1,
|
||||
name: 'Sub451',
|
||||
full_name: 'Commit451 / Sub451',
|
||||
web_url: `${gl.TEST_HOST}/Commit451/Sub451`,
|
||||
avatar_url: null,
|
||||
};
|
||||
|
||||
export const mockFrequentGroups = [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Subgroup451',
|
||||
full_name: 'Commit451 / Subgroup451',
|
||||
webUrl: '/Commit451/Subgroup451',
|
||||
avatarUrl: null,
|
||||
frequency: 7,
|
||||
lastAccessedOn: 1497979281815,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Commit451',
|
||||
full_name: 'Commit451',
|
||||
webUrl: '/Commit451',
|
||||
avatarUrl: null,
|
||||
frequency: 3,
|
||||
lastAccessedOn: 1497979281815,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSearchedGroups = [mockRawGroup];
|
||||
export const mockProcessedSearchedGroups = [mockGroup];
|
||||
|
||||
export const mockProject = {
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
namespace: 'gitlab-org / gitlab-ce',
|
||||
webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatarUrl: null,
|
||||
};
|
||||
|
||||
export const mockRawProject = {
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
name_with_namespace: 'gitlab-org / gitlab-ce',
|
||||
web_url: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatar_url: null,
|
||||
};
|
||||
|
||||
export const mockFrequentProjects = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'GitLab Community Edition',
|
||||
namespace: 'gitlab-org / gitlab-ce',
|
||||
webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-foss`,
|
||||
avatarUrl: null,
|
||||
frequency: 1,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'GitLab CI',
|
||||
namespace: 'gitlab-org / gitlab-ci',
|
||||
webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ci`,
|
||||
avatarUrl: null,
|
||||
frequency: 9,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Typeahead.Js',
|
||||
namespace: 'twitter / typeahead-js',
|
||||
webUrl: `${gl.TEST_HOST}/twitter/typeahead-js`,
|
||||
avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
|
||||
frequency: 2,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Intel',
|
||||
namespace: 'platform / hardware / bsp / intel',
|
||||
webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/intel`,
|
||||
avatarUrl: null,
|
||||
frequency: 3,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'v4.4',
|
||||
namespace: 'platform / hardware / bsp / kernel / common / v4.4',
|
||||
webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`,
|
||||
avatarUrl: null,
|
||||
frequency: 8,
|
||||
lastAccessedOn: Date.now(),
|
||||
},
|
||||
];
|
||||
|
||||
export const mockSearchedProjects = { data: [mockRawProject] };
|
||||
export const mockProcessedSearchedProjects = [mockProject];
|
||||
|
||||
export const unsortedFrequentItems = [
|
||||
{ id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
|
||||
{ id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
|
||||
{ id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
|
||||
{ id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
|
||||
{ id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
|
||||
{ id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
|
||||
{ id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
|
||||
{ id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
|
||||
{ id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
|
||||
];
|
||||
|
||||
/**
|
||||
* This const has a specific order which tests authenticity
|
||||
* of `getTopFrequentItems` method so
|
||||
* DO NOT change order of items in this const.
|
||||
*/
|
||||
export const sortedFrequentItems = [
|
||||
{ id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
|
||||
{ id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
|
||||
{ id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
|
||||
{ id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
|
||||
{ id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
|
||||
{ id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
|
||||
{ id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
|
||||
{ id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
|
||||
{ id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
|
||||
];
|
|
@ -2,6 +2,6 @@
|
|||
// file this one re-exports from. For more detail about why, see:
|
||||
// https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/31349
|
||||
|
||||
import mockData from '../../../spec/frontend/sidebar/mock_data';
|
||||
import mockData from '../../frontend/sidebar/mock_data';
|
||||
|
||||
export default mockData;
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require Rails.root.join('db', 'post_migrate', '20200511080113_add_projects_foreign_key_to_namespaces.rb')
|
||||
require Rails.root.join('db', 'post_migrate', '20200511083541_cleanup_projects_with_missing_namespace.rb')
|
||||
|
||||
LOST_AND_FOUND_GROUP = 'lost-and-found'
|
||||
USER_TYPE_GHOST = 5
|
||||
ACCESS_LEVEL_OWNER = 50
|
||||
|
||||
# In order to test the CleanupProjectsWithMissingNamespace migration, we need
|
||||
# to first create an orphaned project (one with an invalid namespace_id)
|
||||
# and then run the migration to check that the project was properly cleaned up
|
||||
#
|
||||
# The problem is that the CleanupProjectsWithMissingNamespace migration comes
|
||||
# after the FK has been added with a previous migration (AddProjectsForeignKeyToNamespaces)
|
||||
# That means that while testing the current class we can not insert projects with an
|
||||
# invalid namespace_id as the existing FK is correctly blocking us from doing so
|
||||
#
|
||||
# The approach that solves that problem is to:
|
||||
# - Set the schema of this test to the one prior to AddProjectsForeignKeyToNamespaces
|
||||
# - We could hardcode it to `20200508091106` (which currently is the previous
|
||||
# migration before adding the FK) but that would mean that this test depends
|
||||
# on migration 20200508091106 not being reverted or deleted
|
||||
# - So, we use SchemaVersionFinder that finds the previous migration and returns
|
||||
# its schema, which we then use in the describe
|
||||
#
|
||||
# That means that we lock the schema version to the one returned by
|
||||
# SchemaVersionFinder.previous_migration and only test the cleanup migration
|
||||
# *without* the migration that adds the Foreign Key ever running
|
||||
# That's acceptable as the cleanup script should not be affected in any way
|
||||
# by the migration that adds the Foreign Key
|
||||
class SchemaVersionFinder
|
||||
def self.migrations_paths
|
||||
ActiveRecord::Migrator.migrations_paths
|
||||
end
|
||||
|
||||
def self.migration_context
|
||||
ActiveRecord::MigrationContext.new(migrations_paths, ActiveRecord::SchemaMigration)
|
||||
end
|
||||
|
||||
def self.migrations
|
||||
migration_context.migrations
|
||||
end
|
||||
|
||||
def self.previous_migration
|
||||
migrations.each_cons(2) do |previous, migration|
|
||||
break previous.version if migration.name == AddProjectsForeignKeyToNamespaces.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe CleanupProjectsWithMissingNamespace, :migration, schema: SchemaVersionFinder.previous_migration do
|
||||
let(:projects) { table(:projects) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:users) { table(:users) }
|
||||
|
||||
before do
|
||||
namespace = namespaces.create!(name: 'existing_namespace', path: 'existing_namespace')
|
||||
|
||||
projects.create!(
|
||||
name: 'project_with_existing_namespace',
|
||||
path: 'project_with_existing_namespace',
|
||||
visibility_level: 20,
|
||||
archived: false,
|
||||
namespace_id: namespace.id
|
||||
)
|
||||
|
||||
projects.create!(
|
||||
name: 'project_with_non_existing_namespace',
|
||||
path: 'project_with_non_existing_namespace',
|
||||
visibility_level: 20,
|
||||
archived: false,
|
||||
namespace_id: non_existing_record_id
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates the ghost user' do
|
||||
expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(0)
|
||||
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
expect(users.where(user_type: USER_TYPE_GHOST).count).to eq(1)
|
||||
end
|
||||
|
||||
it 'creates the lost-and-found group, owned by the ghost user' do
|
||||
expect(
|
||||
Group.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%")).count
|
||||
).to eq(0)
|
||||
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
ghost_user = users.find_by(user_type: USER_TYPE_GHOST)
|
||||
expect(
|
||||
Group
|
||||
.joins('INNER JOIN members ON namespaces.id = members.source_id')
|
||||
.where('namespaces.type = ?', 'Group')
|
||||
.where('members.type = ?', 'GroupMember')
|
||||
.where('members.source_type = ?', 'Namespace')
|
||||
.where('members.user_id = ?', ghost_user.id)
|
||||
.where('members.requested_at IS NULL')
|
||||
.where('members.access_level = ?', ACCESS_LEVEL_OWNER)
|
||||
.where(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
|
||||
.count
|
||||
).to eq(1)
|
||||
end
|
||||
|
||||
it 'moves the orphaned project to the lost-and-found group' do
|
||||
orphaned_project = projects.find_by(name: 'project_with_non_existing_namespace')
|
||||
expect(orphaned_project.visibility_level).to eq(20)
|
||||
expect(orphaned_project.archived).to eq(false)
|
||||
expect(orphaned_project.namespace_id).to eq(non_existing_record_id)
|
||||
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
lost_and_found_group = Group.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
|
||||
orphaned_project = projects.find_by(id: orphaned_project.id)
|
||||
|
||||
expect(orphaned_project.visibility_level).to eq(0)
|
||||
expect(orphaned_project.namespace_id).to eq(lost_and_found_group.id)
|
||||
expect(orphaned_project.name).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
|
||||
expect(orphaned_project.path).to eq("project_with_non_existing_namespace_#{orphaned_project.id}")
|
||||
expect(orphaned_project.archived).to eq(true)
|
||||
|
||||
valid_project = projects.find_by(name: 'project_with_existing_namespace')
|
||||
existing_namespace = namespaces.find_by(name: 'existing_namespace')
|
||||
|
||||
expect(valid_project.visibility_level).to eq(20)
|
||||
expect(valid_project.namespace_id).to eq(existing_namespace.id)
|
||||
expect(valid_project.path).to eq('project_with_existing_namespace')
|
||||
expect(valid_project.archived).to eq(false)
|
||||
end
|
||||
end
|
|
@ -49,6 +49,12 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.without_project_bot' do
|
||||
it 'includes everyone except project_bot' do
|
||||
expect(described_class.without_project_bot).to match_array(everyone - [project_bot])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#bot?' do
|
||||
it 'is true for all bot user types and false for others' do
|
||||
expect(bots).to all(be_bot)
|
||||
|
|
|
@ -4012,16 +4012,6 @@ describe Project do
|
|||
expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
|
||||
end
|
||||
|
||||
it 'is a no-op when there is no namespace' do
|
||||
project.namespace.delete
|
||||
project.reload
|
||||
|
||||
expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
|
||||
expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
|
||||
|
||||
expect { project.remove_pages }.not_to change { pages_metadatum.reload.deployed }
|
||||
end
|
||||
|
||||
it 'is run when the project is destroyed' do
|
||||
expect(project).to receive(:remove_pages).and_call_original
|
||||
|
||||
|
|
|
@ -79,19 +79,5 @@ describe NamespacelessProjectDestroyWorker do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'project has non-existing namespace' do
|
||||
let!(:project) do
|
||||
project = build(:project, namespace_id: non_existing_record_id)
|
||||
project.save(validate: false)
|
||||
project
|
||||
end
|
||||
|
||||
it 'deletes the project' do
|
||||
subject.perform(project.id)
|
||||
|
||||
expect(Project.unscoped.all).not_to include(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
138
yarn.lock
138
yarn.lock
|
@ -1957,15 +1957,6 @@ axios@^0.19.0:
|
|||
follow-redirects "1.5.10"
|
||||
is-buffer "^2.0.2"
|
||||
|
||||
babel-code-frame@^6.26.0:
|
||||
version "6.26.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
|
||||
integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=
|
||||
dependencies:
|
||||
chalk "^1.1.3"
|
||||
esutils "^2.0.2"
|
||||
js-tokens "^3.0.2"
|
||||
|
||||
babel-eslint@^10.0.3:
|
||||
version "10.0.3"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.3.tgz#81a2c669be0f205e19462fed2482d33e4687a88a"
|
||||
|
@ -2543,7 +2534,7 @@ camelcase@^4.0.0, camelcase@^4.1.0:
|
|||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
|
||||
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
|
||||
|
||||
camelcase@^5.0.0:
|
||||
camelcase@^5.0.0, camelcase@^5.2.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
|
@ -2591,7 +2582,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1, chalk@^2.4.
|
|||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^1.1.1, chalk@^1.1.3:
|
||||
chalk@^1.1.1:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
|
||||
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
|
||||
|
@ -3283,38 +3274,28 @@ css-b64-images@~0.2.5:
|
|||
resolved "https://registry.yarnpkg.com/css-b64-images/-/css-b64-images-0.2.5.tgz#42005d83204b2b4a5d93b6b1a5644133b5927a02"
|
||||
integrity sha1-QgBdgyBLK0pdk7axpWRBM7WSegI=
|
||||
|
||||
css-loader@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-1.0.1.tgz#6885bb5233b35ec47b006057da01cc640b6b79fe"
|
||||
integrity sha512-+ZHAZm/yqvJ2kDtPne3uX0C+Vr3Zn5jFn2N4HywtS5ujwvsVkyg0VArEXpl3BgczDA8anieki1FIzhchX4yrDw==
|
||||
css-loader@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-2.1.1.tgz#d8254f72e412bb2238bb44dd674ffbef497333ea"
|
||||
integrity sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==
|
||||
dependencies:
|
||||
babel-code-frame "^6.26.0"
|
||||
css-selector-tokenizer "^0.7.0"
|
||||
icss-utils "^2.1.0"
|
||||
loader-utils "^1.0.2"
|
||||
lodash "^4.17.11"
|
||||
postcss "^6.0.23"
|
||||
postcss-modules-extract-imports "^1.2.0"
|
||||
postcss-modules-local-by-default "^1.2.0"
|
||||
postcss-modules-scope "^1.1.0"
|
||||
postcss-modules-values "^1.3.0"
|
||||
camelcase "^5.2.0"
|
||||
icss-utils "^4.1.0"
|
||||
loader-utils "^1.2.3"
|
||||
normalize-path "^3.0.0"
|
||||
postcss "^7.0.14"
|
||||
postcss-modules-extract-imports "^2.0.0"
|
||||
postcss-modules-local-by-default "^2.0.6"
|
||||
postcss-modules-scope "^2.1.0"
|
||||
postcss-modules-values "^2.0.0"
|
||||
postcss-value-parser "^3.3.0"
|
||||
source-list-map "^2.0.0"
|
||||
schema-utils "^1.0.0"
|
||||
|
||||
css-selector-parser@^1.3:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
|
||||
integrity sha1-XxrUPi2O77/cME/NOaUhZklD4+s=
|
||||
|
||||
css-selector-tokenizer@^0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.2.tgz#11e5e27c9a48d90284f22d45061c303d7a25ad87"
|
||||
integrity sha512-yj856NGuAymN6r8bn8/Jl46pR+OC3eEvAhfGYDUe7YPtTPAYrSSw4oAniZ9Y8T5B92hjhwTBLUen0/vKPxf6pw==
|
||||
dependencies:
|
||||
cssesc "^3.0.0"
|
||||
fastparse "^1.1.2"
|
||||
regexpu-core "^4.6.0"
|
||||
|
||||
css@^2.1.0:
|
||||
version "2.2.4"
|
||||
resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929"
|
||||
|
@ -4783,11 +4764,6 @@ fast-levenshtein@~2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
|
||||
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
|
||||
|
||||
fastparse@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9"
|
||||
integrity sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==
|
||||
|
||||
fault@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
|
||||
|
@ -5747,12 +5723,12 @@ icss-replace-symbols@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded"
|
||||
integrity sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=
|
||||
|
||||
icss-utils@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-2.1.0.tgz#83f0a0ec378bf3246178b6c2ad9136f135b1c962"
|
||||
integrity sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=
|
||||
icss-utils@^4.1.0:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
|
||||
integrity sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==
|
||||
dependencies:
|
||||
postcss "^6.0.1"
|
||||
postcss "^7.0.14"
|
||||
|
||||
ieee754@1.1.13, ieee754@^1.1.4:
|
||||
version "1.1.13"
|
||||
|
@ -6910,11 +6886,6 @@ js-cookie@^2.2.1:
|
|||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-yaml@^3.13.1:
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
|
||||
|
@ -9017,36 +8988,37 @@ postcss-media-query-parser@^0.2.3:
|
|||
resolved "https://registry.yarnpkg.com/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz#27b39c6f4d94f81b1a73b8f76351c609e5cef244"
|
||||
integrity sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=
|
||||
|
||||
postcss-modules-extract-imports@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz#dc87e34148ec7eab5f791f7cd5849833375b741a"
|
||||
integrity sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==
|
||||
postcss-modules-extract-imports@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz#818719a1ae1da325f9832446b01136eeb493cd7e"
|
||||
integrity sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==
|
||||
dependencies:
|
||||
postcss "^6.0.1"
|
||||
postcss "^7.0.5"
|
||||
|
||||
postcss-modules-local-by-default@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz#f7d80c398c5a393fa7964466bd19500a7d61c069"
|
||||
integrity sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=
|
||||
postcss-modules-local-by-default@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz#dd9953f6dd476b5fd1ef2d8830c8929760b56e63"
|
||||
integrity sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==
|
||||
dependencies:
|
||||
css-selector-tokenizer "^0.7.0"
|
||||
postcss "^6.0.1"
|
||||
postcss "^7.0.6"
|
||||
postcss-selector-parser "^6.0.0"
|
||||
postcss-value-parser "^3.3.1"
|
||||
|
||||
postcss-modules-scope@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz#d6ea64994c79f97b62a72b426fbe6056a194bb90"
|
||||
integrity sha1-1upkmUx5+XtipytCb75gVqGUu5A=
|
||||
postcss-modules-scope@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz#385cae013cc7743f5a7d7602d1073a89eaae62ee"
|
||||
integrity sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==
|
||||
dependencies:
|
||||
css-selector-tokenizer "^0.7.0"
|
||||
postcss "^6.0.1"
|
||||
postcss "^7.0.6"
|
||||
postcss-selector-parser "^6.0.0"
|
||||
|
||||
postcss-modules-values@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz#ecffa9d7e192518389f42ad0e83f72aec456ea20"
|
||||
integrity sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=
|
||||
postcss-modules-values@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz#479b46dc0c5ca3dc7fa5270851836b9ec7152f64"
|
||||
integrity sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==
|
||||
dependencies:
|
||||
icss-replace-symbols "^1.1.0"
|
||||
postcss "^6.0.1"
|
||||
postcss "^7.0.6"
|
||||
|
||||
postcss-reporter@^6.0.1:
|
||||
version "6.0.1"
|
||||
|
@ -9103,7 +9075,7 @@ postcss-selector-parser@^5.0.0:
|
|||
indexes-of "^1.0.1"
|
||||
uniq "^1.0.1"
|
||||
|
||||
postcss-selector-parser@^6.0.2:
|
||||
postcss-selector-parser@^6.0.0, postcss-selector-parser@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz#934cf799d016c83411859e09dcecade01286ec5c"
|
||||
integrity sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==
|
||||
|
@ -9127,15 +9099,6 @@ postcss-value-parser@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz#99a983d365f7b2ad8d0f9b8c3094926eab4b936d"
|
||||
integrity sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==
|
||||
|
||||
postcss@^6.0.1, postcss@^6.0.23:
|
||||
version "6.0.23"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
|
||||
integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
|
||||
dependencies:
|
||||
chalk "^2.4.1"
|
||||
source-map "^0.6.1"
|
||||
supports-color "^5.4.0"
|
||||
|
||||
postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.27, postcss@^7.0.7:
|
||||
version "7.0.27"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
|
||||
|
@ -9145,6 +9108,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2
|
|||
source-map "^0.6.1"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
postcss@^7.0.5, postcss@^7.0.6:
|
||||
version "7.0.30"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.30.tgz#cc9378beffe46a02cbc4506a0477d05fcea9a8e2"
|
||||
integrity sha512-nu/0m+NtIzoubO+xdAlwZl/u5S5vi/y6BCsoL8D+8IxsD3XvBS8X4YEADNIVXKVuQvduiucnRv+vPIqj56EGMQ==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
source-map "^0.6.1"
|
||||
supports-color "^6.1.0"
|
||||
|
||||
prelude-ls@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
|
@ -10937,7 +10909,7 @@ supports-color@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"
|
||||
integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=
|
||||
|
||||
supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0:
|
||||
supports-color@^5.2.0, supports-color@^5.3.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
|
|
Loading…
Reference in New Issue