Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-19 12:08:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-19 12:08:12 +0300
commit57d1bb82549c6713f87f87d5f35eec3d867c83db (patch)
tree22f708344121786e286fd318fbfbfda632200909
parent4e81d9c050bfea4c866329155c17b929d7381340 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.eslintrc.yml1
-rw-r--r--.gitlab/ci/dev-fixtures.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml9
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml13
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml27
-rw-r--r--.rubocop.yml4
-rw-r--r--app/assets/javascripts/alert_management/components/alert_details.vue14
-rw-r--r--app/assets/javascripts/alert_management/graphql/fragments/detailItem.fragment.graphql11
-rw-r--r--app/assets/javascripts/alert_management/graphql/queries/details.query.graphql13
-rw-r--r--app/assets/javascripts/environments/components/container.vue2
-rw-r--r--app/assets/javascripts/environments/components/delete_environment_modal.vue2
-rw-r--r--app/assets/javascripts/error_tracking/store/actions.js2
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue2
-rw-r--r--app/assets/javascripts/jobs/store/state.js2
-rw-r--r--app/assets/javascripts/notes/mixins/discussion_navigation.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js2
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue4
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue4
-rw-r--r--app/controllers/import/github_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/concerns/has_user_type.rb1
-rw-r--r--app/models/members/project_member.rb5
-rw-r--r--changelogs/unreleased/198603-add-foreign-key-on-projects-namespace-id-and-clean-up-ghost-projec.yml5
-rw-r--r--changelogs/unreleased/remove-experimental-indexer-column.yml5
-rw-r--r--changelogs/unreleased/tr-alert-detail-remaining-fields.yml5
-rw-r--r--changelogs/unreleased/update-css-loader.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in---environments-delete_environment_modal-vue.yml (renamed from changelogs/unreleased/update-deprecated-slot-syntax-in---app-assets-javascripts-environments-co.yml)0
-rw-r--r--config/webpack.config.js3
-rw-r--r--db/migrate/20200515155620_add_index_non_requested_project_members_on_source_id_source_type.rb17
-rw-r--r--db/post_migrate/20200428134356_remove_elastic_experimental_indexer_from_application_settings.rb8
-rw-r--r--db/post_migrate/20200511080113_add_projects_foreign_key_to_namespaces.rb27
-rw-r--r--db/post_migrate/20200511083541_cleanup_projects_with_missing_namespace.rb263
-rw-r--r--db/post_migrate/20200511220023_validate_projects_foreign_key_to_namespaces.rb21
-rw-r--r--db/structure.sql11
-rw-r--r--doc/administration/geo/replication/external_database.md13
-rw-r--r--doc/api/instance_level_ci_variables.md25
-rw-r--r--doc/security/README.md4
-rw-r--r--package.json2
-rw-r--r--scripts/rspec_helpers.sh4
-rw-r--r--spec/controllers/application_controller_spec.rb6
-rw-r--r--spec/controllers/groups/settings/repository_controller_spec.rb2
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb2
-rw-r--r--spec/db/schema_spec.rb2
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_manager_spec.js)97
-rw-r--r--spec/frontend/filtered_search/recent_searches_root_spec.js (renamed from spec/javascripts/filtered_search/recent_searches_root_spec.js)8
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js (renamed from spec/javascripts/frequent_items/components/app_spec.js)34
-rw-r--r--spec/frontend/frequent_items/mock_data.js127
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js (renamed from spec/javascripts/frequent_items/store/actions_spec.js)4
-rw-r--r--spec/frontend/frequent_items/store/mutations_spec.js (renamed from spec/javascripts/frequent_items/store/mutations_spec.js)0
-rw-r--r--spec/frontend/frequent_items/utils_spec.js (renamed from spec/javascripts/frequent_items/utils_spec.js)14
-rw-r--r--spec/frontend/helpers/fixtures.js5
-rw-r--r--spec/frontend/lib/utils/csrf_token_spec.js (renamed from spec/javascripts/lib/utils/csrf_token_spec.js)25
-rw-r--r--spec/frontend/lib/utils/navigation_utility_spec.js (renamed from spec/javascripts/lib/utils/navigation_utility_spec.js)6
-rw-r--r--spec/frontend/lib/utils/poll_spec.js (renamed from spec/javascripts/lib/utils/poll_spec.js)123
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js (renamed from spec/javascripts/lib/utils/sticky_spec.js)23
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js7
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js2
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js2
-rw-r--r--spec/frontend/related_merge_requests/components/related_merge_requests_spec.js (renamed from spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js)8
-rw-r--r--spec/frontend/related_merge_requests/store/actions_spec.js (renamed from spec/javascripts/related_merge_requests/store/actions_spec.js)13
-rw-r--r--spec/frontend/related_merge_requests/store/mutations_spec.js (renamed from spec/javascripts/related_merge_requests/store/mutations_spec.js)0
-rw-r--r--spec/javascripts/frequent_items/mock_data.js168
-rw-r--r--spec/javascripts/sidebar/mock_data.js2
-rw-r--r--spec/migrations/cleanup_projects_with_missing_namespace_spec.rb134
-rw-r--r--spec/models/concerns/has_user_type_spec.rb6
-rw-r--r--spec/models/project_spec.rb10
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb14
-rw-r--r--yarn.lock138
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==