Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-19 09:08:12 +00:00
parent 4e81d9c050
commit 57d1bb8254
81 changed files with 1019 additions and 550 deletions

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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"

View File

@ -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/**/*'

View File

@ -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"

View File

@ -0,0 +1,11 @@
#import "./listItem.fragment.graphql"
fragment AlertDetailItem on AlertManagementAlert {
...AlertListItem
createdAt
monitoringTool
service
description
updatedAt
details
}

View File

@ -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
}
}
}

View File

@ -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: {

View File

@ -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">

View File

@ -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';

View File

@ -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';

View File

@ -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: {

View File

@ -1,4 +1,4 @@
import { isNewJobLogActive } from '../store/utils';
import { isNewJobLogActive } from './utils';
export default () => ({
jobEndpoint: null,

View File

@ -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

View File

@ -1,3 +1,3 @@
import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits';
import initUserInternalRegexPlaceholder from '../account_and_limits';
document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder());

View File

@ -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);

View File

@ -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

View File

@ -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: {

View File

@ -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';

View File

@ -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 {

View File

@ -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';

View File

@ -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.

View File

@ -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: {

View File

@ -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;

View File

@ -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">`;

View File

@ -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';

View File

@ -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 }

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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
#

View File

@ -0,0 +1,5 @@
---
title: Add Foreign Key on projects.namespaces_id
merge_request: 31675
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Remove elasticsearch_experimental_indexer column
merge_request: 30628
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add fields to Alert Details view
merge_request: 32392
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update css-loader ^1.0.0 -> ^2.1.1
merge_request: 31743
author: Pirate Praveen
type: other

View File

@ -232,7 +232,8 @@ module.exports = {
{
loader: 'css-loader',
options: {
name: '[name].[contenthash:8].[ext]',
modules: 'global',
localIdentName: '[name].[contenthash:8].[ext]',
},
},
],

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
\.

View File

@ -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

View File

@ -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)
```

View File

@ -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.

View File

@ -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",

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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'
},

View File

@ -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

View File

@ -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],

View File

@ -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]);
});
});
});

View File

@ -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"');
});

View File

@ -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,

View File

@ -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 },
];

View File

@ -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;

View File

@ -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);

View File

@ -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);
};

View File

@ -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">
`);

View File

@ -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');

View File

@ -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();

View File

@ -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', () => {

View File

@ -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');

View File

@ -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';

View File

@ -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;

View File

@ -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';

View File

@ -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(() => {

View File

@ -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();
},

View File

@ -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 },
];

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
View File

@ -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==