diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-19 12:08:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-19 12:08:12 +0300 |
commit | 57d1bb82549c6713f87f87d5f35eec3d867c83db (patch) | |
tree | 22f708344121786e286fd318fbfbfda632200909 | |
parent | 4e81d9c050bfea4c866329155c17b929d7381340 (diff) |
Add latest changes from gitlab-org/gitlab@master
81 files changed, 1019 insertions, 550 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index 0cd1e7c5ec9..f8bc2a3ae94 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -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 diff --git a/.gitlab/ci/dev-fixtures.gitlab-ci.yml b/.gitlab/ci/dev-fixtures.gitlab-ci.yml index 47acf8facc8..fc3678a7d17 100644 --- a/.gitlab/ci/dev-fixtures.gitlab-ci.yml +++ b/.gitlab/ci/dev-fixtures.gitlab-ci.yml @@ -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" diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 1538352a881..6e9119f295a 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -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 diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 74193b87e27..e6619ff2b6d 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -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: diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 0bf90e18bbd..e8087aebcef 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -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" diff --git a/.rubocop.yml b/.rubocop.yml index 6687ada610a..439a22d00d3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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/**/*' diff --git a/app/assets/javascripts/alert_management/components/alert_details.vue b/app/assets/javascripts/alert_management/components/alert_details.vue index d042336c361..272caa11e03 100644 --- a/app/assets/javascripts/alert_management/components/alert_details.vue +++ b/app/assets/javascripts/alert_management/components/alert_details.vue @@ -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" diff --git a/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql b/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql new file mode 100644 index 00000000000..df802616e97 --- /dev/null +++ b/app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql @@ -0,0 +1,11 @@ +#import "./listItem.fragment.graphql" + +fragment AlertDetailItem on AlertManagementAlert { + ...AlertListItem + createdAt + monitoringTool + service + description + updatedAt + details +} diff --git a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql index 3e86df233d0..7c77715fad2 100644 --- a/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql +++ b/app/assets/javascripts/alert_management/graphql/queries/details.query.graphql @@ -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 } } } diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 5c391d68c4c..899d7ec8521 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -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: { diff --git a/app/assets/javascripts/environments/components/delete_environment_modal.vue b/app/assets/javascripts/environments/components/delete_environment_modal.vue index f731dc49a5b..29aab268fd3 100644 --- a/app/assets/javascripts/environments/components/delete_environment_modal.vue +++ b/app/assets/javascripts/environments/components/delete_environment_modal.vue @@ -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"> diff --git a/app/assets/javascripts/error_tracking/store/actions.js b/app/assets/javascripts/error_tracking/store/actions.js index 8f6f404ef8a..05554b2b566 100644 --- a/app/assets/javascripts/error_tracking/store/actions.js +++ b/app/assets/javascripts/error_tracking/store/actions.js @@ -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'; diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 1f1776a5487..61080fb5487 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -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'; diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index 9ad17d73716..2798ede5341 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.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: { diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js index 5a61828ec6d..d76828ad19b 100644 --- a/app/assets/javascripts/jobs/store/state.js +++ b/app/assets/javascripts/jobs/store/state.js @@ -1,4 +1,4 @@ -import { isNewJobLogActive } from '../store/utils'; +import { isNewJobLogActive } from './utils'; export default () => ({ jobEndpoint: null, diff --git a/app/assets/javascripts/notes/mixins/discussion_navigation.js b/app/assets/javascripts/notes/mixins/discussion_navigation.js index 08c7efd69a6..c9026352d18 100644 --- a/app/assets/javascripts/notes/mixins/discussion_navigation.js +++ b/app/assets/javascripts/notes/mixins/discussion_navigation.js @@ -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 diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js index 5ec9688a6e4..8183e81fb02 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js @@ -1,3 +1,3 @@ -import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits'; +import initUserInternalRegexPlaceholder from '../account_and_limits'; document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder()); diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js index 6ae1dbb72c4..a318aa2a694 100644 --- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js +++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js @@ -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); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index f16b16a6837..3baf4bf0742 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -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 diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue index 3957547556c..dff21d919a9 100644 --- a/app/assets/javascripts/static_site_editor/components/edit_area.vue +++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue @@ -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: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue index 5b578e42f91..92848e86e76 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue @@ -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'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue index a5c75369fa1..302a30dab54 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -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 { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 7279aaf0809..1a6e186a371 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -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'; diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index 162cfc02959..890dbe86c0d 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -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. diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 356f733fb8c..23bea6c28b4 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -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: { diff --git a/app/assets/javascripts/vue_shared/components/file_finder/item.vue b/app/assets/javascripts/vue_shared/components/file_finder/item.vue index 73511879ff2..018e3a84c39 100644 --- a/app/assets/javascripts/vue_shared/components/file_finder/item.vue +++ b/app/assets/javascripts/vue_shared/components/file_finder/item.vue @@ -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; diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index c1b18a7b2f7..cb3cd18e5a7 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -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">`; diff --git a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue index 2bf3bc31cde..4f1b1c758b2 100644 --- a/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue +++ b/app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue @@ -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'; diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index 34af1ecd6a5..4e8ceae75bd 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -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 } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index f6eb0101ae0..2f86b945b06 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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), diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 498d6823cc7..b29d6731b08 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -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 diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb index 1347e0be637..8a238dc736c 100644 --- a/app/models/concerns/has_user_type.rb +++ b/app/models/concerns/has_user_type.rb @@ -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 diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 68c51860c47..fa2e0cb8198 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -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 # diff --git a/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml b/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml new file mode 100644 index 00000000000..87481ee524b --- /dev/null +++ b/changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml @@ -0,0 +1,5 @@ +--- +title: Add Foreign Key on projects.namespaces_id +merge_request: 31675 +author: +type: other diff --git a/changelogs/unreleased/remove-experimental-indexer-column.yml b/changelogs/unreleased/remove-experimental-indexer-column.yml new file mode 100644 index 00000000000..7e8f389cc3c --- /dev/null +++ b/changelogs/unreleased/remove-experimental-indexer-column.yml @@ -0,0 +1,5 @@ +--- +title: Remove elasticsearch_experimental_indexer column +merge_request: 30628 +author: +type: other diff --git a/changelogs/unreleased/tr-alert-detail-remaining-fields.yml b/changelogs/unreleased/tr-alert-detail-remaining-fields.yml new file mode 100644 index 00000000000..e1b12ecd5eb --- /dev/null +++ b/changelogs/unreleased/tr-alert-detail-remaining-fields.yml @@ -0,0 +1,5 @@ +--- +title: Add fields to Alert Details view +merge_request: 32392 +author: +type: added diff --git a/changelogs/unreleased/update-css-loader.yml b/changelogs/unreleased/update-css-loader.yml new file mode 100644 index 00000000000..f164c9f4e30 --- /dev/null +++ b/changelogs/unreleased/update-css-loader.yml @@ -0,0 +1,5 @@ +--- +title: Update css-loader ^1.0.0 -> ^2.1.1 +merge_request: 31743 +author: Pirate Praveen +type: other diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-environments-co.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml index 317da0d88f5..317da0d88f5 100644 --- a/changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-environments-co.yml +++ b/changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml diff --git a/config/webpack.config.js b/config/webpack.config.js index be670515267..7c130b010b6 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -232,7 +232,8 @@ module.exports = { { loader: 'css-loader', options: { - name: '[name].[contenthash:8].[ext]', + modules: 'global', + localIdentName: '[name].[contenthash:8].[ext]', }, }, ], diff --git a/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb b/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb new file mode 100644 index 00000000000..333f4e93e95 --- /dev/null +++ b/db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb @@ -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 diff --git a/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb b/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb new file mode 100644 index 00000000000..a9baf6fd8e3 --- /dev/null +++ b/db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb @@ -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 diff --git a/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb b/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb new file mode 100644 index 00000000000..a7f67a3b5cd --- /dev/null +++ b/db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb @@ -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 diff --git a/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb new file mode 100644 index 00000000000..442acfc6d16 --- /dev/null +++ b/db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb @@ -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 diff --git a/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb b/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb new file mode 100644 index 00000000000..37a761507fc --- /dev/null +++ b/db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb @@ -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 diff --git a/db/structure.sql b/db/structure.sql index 3c1bb807ce9..38a8f98a1f3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -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 \. diff --git a/doc/administration/geo/replication/external_database.md b/doc/administration/geo/replication/external_database.md index 2ca7b309248..ae3069a0e40 100644 --- a/doc/administration/geo/replication/external_database.md +++ b/doc/administration/geo/replication/external_database.md @@ -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 diff --git a/doc/api/instance_level_ci_variables.md b/doc/api/instance_level_ci_variables.md index c5085bebe91..d0871fdf4a7 100644 --- a/doc/api/instance_level_ci_variables.md +++ b/doc/api/instance_level_ci_variables.md @@ -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) +``` diff --git a/doc/security/README.md b/doc/security/README.md index c21d99658b8..e2375c0f0b5 100644 --- a/doc/security/README.md +++ b/doc/security/README.md @@ -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. diff --git a/package.json b/package.json index b43bda5f9d6..82976399171 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh index adaae0df870..0c9d3505ff3 100644 --- a/scripts/rspec_helpers.sh +++ b/scripts/rspec_helpers.sh @@ -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" diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b406c184b88..ed2e61d6cf6 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -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) diff --git a/spec/controllers/groups/settings/repository_controller_spec.rb b/spec/controllers/groups/settings/repository_controller_spec.rb index f883cdba72e..9523d404538 100644 --- a/spec/controllers/groups/settings/repository_controller_spec.rb +++ b/spec/controllers/groups/settings/repository_controller_spec.rb @@ -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 diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb index f80ad30faaa..e589815c45d 100644 --- a/spec/controllers/projects/graphs_controller_spec.rb +++ b/spec/controllers/projects/graphs_controller_spec.rb @@ -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' }, diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb index 11bac12f92d..fb9cdd860dc 100644 --- a/spec/controllers/projects/settings/repository_controller_spec.rb +++ b/spec/controllers/projects/settings/repository_controller_spec.rb @@ -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 diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index fdf79f24bd2..eac9eb7aa47 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -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], diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js index d0b54a16747..ef87662a1ef 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js @@ -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]); }); }); }); diff --git a/spec/javascripts/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js index 70dd4e9570d..281d406e013 100644 --- a/spec/javascripts/filtered_search/recent_searches_root_spec.js +++ b/spec/frontend/filtered_search/recent_searches_root_spec.js @@ -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"'); }); diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js index b293ed541fd..7c54a48aa41 100644 --- a/spec/javascripts/frequent_items/components/app_spec.js +++ b/spec/frontend/frequent_items/components/app_spec.js @@ -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, diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js index 5cd4cddd877..8c3c66f67ff 100644 --- a/spec/frontend/frequent_items/mock_data.js +++ b/spec/frontend/frequent_items/mock_data.js @@ -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 }, +]; diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js index 7b065b69cce..304098e85f1 100644 --- a/spec/javascripts/frequent_items/store/actions_spec.js +++ b/spec/frontend/frequent_items/store/actions_spec.js @@ -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; diff --git a/spec/javascripts/frequent_items/store/mutations_spec.js b/spec/frontend/frequent_items/store/mutations_spec.js index d36964b2600..d36964b2600 100644 --- a/spec/javascripts/frequent_items/store/mutations_spec.js +++ b/spec/frontend/frequent_items/store/mutations_spec.js diff --git a/spec/javascripts/frequent_items/utils_spec.js b/spec/frontend/frequent_items/utils_spec.js index 2939b46bc31..181dd9268dc 100644 --- a/spec/javascripts/frequent_items/utils_spec.js +++ b/spec/frontend/frequent_items/utils_spec.js @@ -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); diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js index 778196843db..a89ceab3f8e 100644 --- a/spec/frontend/helpers/fixtures.js +++ b/spec/frontend/helpers/fixtures.js @@ -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); }; diff --git a/spec/javascripts/lib/utils/csrf_token_spec.js b/spec/frontend/lib/utils/csrf_token_spec.js index 867bee34ee5..1b98ef126e9 100644 --- a/spec/javascripts/lib/utils/csrf_token_spec.js +++ b/spec/frontend/lib/utils/csrf_token_spec.js @@ -1,37 +1,44 @@ import csrf from '~/lib/utils/csrf'; +import { setHTMLFixture } from 'helpers/fixtures'; + +describe('csrf', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); -describe('csrf', function() { beforeEach(() => { - this.tokenKey = 'X-CSRF-Token'; - this.token = + 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"> `); diff --git a/spec/javascripts/lib/utils/navigation_utility_spec.js b/spec/frontend/lib/utils/navigation_utility_spec.js index be620e4a27c..88172f38894 100644 --- a/spec/javascripts/lib/utils/navigation_utility_spec.js +++ b/spec/frontend/lib/utils/navigation_utility_spec.js @@ -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'); diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js index 138041a349f..5ee9738ebf3 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/frontend/lib/utils/poll_spec.js @@ -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(); diff --git a/spec/javascripts/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js index 1b1e7da1ed3..4ad68cc9ff6 100644 --- a/spec/javascripts/lib/utils/sticky_spec.js +++ b/spec/frontend/lib/utils/sticky_spec.js @@ -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', () => { diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index a2c7f0b3767..dc68c4371aa 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -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'); diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js index 5101b81e3ee..44dc148933c 100644 --- a/spec/frontend/notes/components/discussion_actions_spec.js +++ b/spec/frontend/notes/components/discussion_actions_spec.js @@ -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'; diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js index 81773752037..5a10deefd09 100644 --- a/spec/frontend/notes/components/discussion_notes_spec.js +++ b/spec/frontend/notes/components/discussion_notes_spec.js @@ -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; diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js index e22dd85f221..fbfba2efb1d 100644 --- a/spec/frontend/notes/components/notes_app_spec.js +++ b/spec/frontend/notes/components/notes_app_spec.js @@ -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'; diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js index d8bdf69dfee..1b938c93df8 100644 --- a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js +++ b/spec/frontend/related_merge_requests/components/related_merge_requests_spec.js @@ -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(() => { diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js index c4cd9f5f803..26c5977cb5f 100644 --- a/spec/javascripts/related_merge_requests/store/actions_spec.js +++ b/spec/frontend/related_merge_requests/store/actions_spec.js @@ -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(); }, diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/frontend/related_merge_requests/store/mutations_spec.js index 21b6e26376b..21b6e26376b 100644 --- a/spec/javascripts/related_merge_requests/store/mutations_spec.js +++ b/spec/frontend/related_merge_requests/store/mutations_spec.js diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js deleted file mode 100644 index 419f70e41af..00000000000 --- a/spec/javascripts/frequent_items/mock_data.js +++ /dev/null @@ -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 }, -]; diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index c869ff96933..701a97ba5a6 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -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; diff --git a/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb new file mode 100644 index 00000000000..06b6d5e3b46 --- /dev/null +++ b/spec/migrations/cleanup_projects_with_missing_namespace_spec.rb @@ -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 diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb index 7ec29dde57b..f12eee414f9 100644 --- a/spec/models/concerns/has_user_type_spec.rb +++ b/spec/models/concerns/has_user_type_spec.rb @@ -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) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7fbca7b8105..5f8b51c250d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -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 diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb index b97a44c714d..ceea7c8d8f5 100644 --- a/spec/workers/namespaceless_project_destroy_worker_spec.rb +++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb @@ -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 diff --git a/yarn.lock b/yarn.lock index e38111fa438..f6672fc96c8 100644 --- a/yarn.lock +++ b/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== |