diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index e54d970c671..a0bbd6fa9a0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -25,7 +25,6 @@ Layout/ClosingHeredocIndentation: - 'app/graphql/mutations/merge_requests/set_wip.rb' - 'ee/db/geo/migrate/20180322062741_migrate_ci_job_artifacts_to_separate_registry.rb' - 'ee/lib/gitlab/geo/health_check.rb' - - 'lib/gitlab/background_migration/populate_untracked_uploads.rb' - 'spec/features/merge_request/user_sees_diff_spec.rb' - 'spec/lib/gitlab/asciidoc_spec.rb' - 'spec/lib/gitlab/checks/project_moved_spec.rb' @@ -1536,7 +1535,6 @@ Rails/SaveBang: - 'spec/support/helpers/stub_object_storage.rb' - 'spec/support/migrations_helpers/cluster_helpers.rb' - 'spec/support/migrations_helpers/namespaces_helper.rb' - - 'spec/support/migrations_helpers/track_untracked_uploads_helpers.rb' - 'spec/support/shared_contexts/email_shared_context.rb' - 'spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb' - 'spec/support/shared_contexts/mailers/notify_shared_context.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index c4ace480119..470e80d7471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2185,7 +2185,7 @@ entry. - Replaced ACE with Monaco editor for Snippets. !25465 - Add support for user Job Title. !25483 - Add name_regex_keep param to container registry bulk delete API endpoint. !25484 -- Add Project template for Gatsby. !25486 +- Add Project template for Gatsby. !24192 - Add filepath to ReleaseLink. !25512 - Added Drop older active deployments project setting. !25520 - Add filepath to release links API. !25533 diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index ac445a1d9f1..b2e2b152c40 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -167,7 +167,7 @@ export default { }, mounted() { if (!this.editor) { - this.editor = Editor.create(this.editorOptions); + this.editor = Editor.create(this.$store, this.editorOptions); } this.initEditor(); diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 850cfcb05e3..0e77fecfe76 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -3,7 +3,7 @@ import { mapActions } from 'vuex'; import Translate from '~/vue_shared/translate'; import { identity } from 'lodash'; import ide from './components/ide.vue'; -import store from './stores'; +import { createStore } from './stores'; import { createRouter } from './ide_router'; import { parseBoolean } from '../lib/utils/common_utils'; import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; @@ -32,6 +32,7 @@ export function initIde(el, options = {}) { if (!el) return null; const { rootComponent = ide, extendStore = identity } = options; + const store = createStore(); const router = createRouter(store); return new Vue({ diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 6e90968f008..f061fcb1259 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,6 +1,5 @@ import { debounce } from 'lodash'; import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor'; -import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; import Disposable from './common/disposable'; @@ -20,14 +19,14 @@ function setupThemes() { } export default class Editor { - static create(options = {}) { + static create(...args) { if (!this.editorInstance) { - this.editorInstance = new Editor(options); + this.editorInstance = new Editor(...args); } return this.editorInstance; } - constructor(options = {}) { + constructor(store, options = {}) { this.currentModel = null; this.instance = null; this.dirtyDiffController = null; @@ -42,6 +41,7 @@ export default class Editor { ...defaultDiffEditorOptions, ...options, }; + this.store = store; setupThemes(); registerLanguages(...languages); @@ -215,6 +215,7 @@ export default class Editor { } addCommands() { + const { store } = this; const getKeyCode = key => { const monacoKeyMod = key.indexOf('KEY_') === 0; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index 18c466cc93d..324c5b0c6e4 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -33,5 +33,3 @@ export const createStoreOptions = () => ({ }); export const createStore = () => new Vuex.Store(createStoreOptions()); - -export default createStore(); diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue index fe6ca3a2a07..db1267d706f 100644 --- a/app/assets/javascripts/monitoring/components/dashboard_header.vue +++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue @@ -254,16 +254,8 @@ export default { {{ __('Environment') }} - - + +
e - msg = <<~MSG - Error parsing path "#{file.path}": - #{e.message} - #{e.backtrace.join("\n ")} - MSG - Rails.logger.error(msg) # rubocop:disable Gitlab/RailsLogger - false - end - end - - def filter_existing_uploads(files) - paths = files.map(&:upload_path) - existing_paths = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Upload.where(path: paths).pluck(:path).to_set - - files.reject do |file| - existing_paths.include?(file.upload_path) - end - end - - # There are files on disk that are not in the uploads table because their - # model was deleted, and we don't delete the files on disk. - def filter_deleted_models(files) - ids = deleted_model_ids(files) - - files.reject do |file| - ids[file.model_type].include?(file.model_id) - end - end - - def deleted_model_ids(files) - ids = { - 'Appearance' => [], - 'Namespace' => [], - 'Note' => [], - 'Project' => [], - 'User' => [] - } - - # group model IDs by model type - files.each do |file| - ids[file.model_type] << file.model_id - end - - ids.each do |model_type, model_ids| - model_class = "Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::#{model_type}".constantize - found_ids = model_class.where(id: model_ids.uniq).pluck(:id) - deleted_ids = ids[model_type] - found_ids - ids[model_type] = deleted_ids - end - - ids - end - - def insert(files) - rows = files.map do |file| - file.to_h.merge(created_at: 'NOW()') - end - - Gitlab::Database.bulk_insert('uploads', # rubocop:disable Gitlab/BulkInsert - rows, - disable_quote: :created_at) - end - - def drop_temp_table_if_finished - if Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup - Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile.connection.drop_table(:untracked_files_for_uploads, - if_exists: true) - end - end - end - end -end diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb deleted file mode 100644 index 23e8be4a9ab..00000000000 --- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb +++ /dev/null @@ -1,190 +0,0 @@ -# frozen_string_literal: true -module Gitlab - module BackgroundMigration - module PopulateUntrackedUploadsDependencies - # This class is responsible for producing the attributes necessary to - # track an uploaded file in the `uploads` table. - class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength - self.table_name = 'untracked_files_for_uploads' - - # Ends with /:random_hex/:filename - FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}.freeze - FULL_PATH_CAPTURE = /\A(.+)#{FILE_UPLOADER_PATH}/.freeze - - # These regex patterns are tested against a relative path, relative to - # the upload directory. - # For convenience, if there exists a capture group in the pattern, then - # it indicates the model_id. - PATH_PATTERNS = [ - { - pattern: %r{\A-/system/appearance/logo/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Appearance' - }, - { - pattern: %r{\A-/system/appearance/header_logo/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Appearance' - }, - { - pattern: %r{\A-/system/note/attachment/(\d+)/}, - uploader: 'AttachmentUploader', - model_type: 'Note' - }, - { - pattern: %r{\A-/system/user/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'User' - }, - { - pattern: %r{\A-/system/group/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'Namespace' - }, - { - pattern: %r{\A-/system/project/avatar/(\d+)/}, - uploader: 'AvatarUploader', - model_type: 'Project' - }, - { - pattern: FILE_UPLOADER_PATH, - uploader: 'FileUploader', - model_type: 'Project' - } - ].freeze - - def to_h - @upload_hash ||= { - path: upload_path, - uploader: uploader, - model_type: model_type, - model_id: model_id, - size: file_size, - checksum: checksum - } - end - - def upload_path - # UntrackedFile#path is absolute, but Upload#path depends on uploader - @upload_path ||= - if uploader == 'FileUploader' - # Path relative to project directory in uploads - matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH) - matchd[0].sub(%r{\A/}, '') # remove leading slash - else - path - end - end - - def uploader - matching_pattern_map[:uploader] - end - - def model_type - matching_pattern_map[:model_type] - end - - def model_id - return @model_id if defined?(@model_id) - - pattern = matching_pattern_map[:pattern] - matchd = path_relative_to_upload_dir.match(pattern) - - # If something is captured (matchd[1] is not nil), it is a model_id - # Only the FileUploader pattern will not match an ID - @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id - end - - def file_size - File.size(absolute_path) - end - - def checksum - Digest::SHA256.file(absolute_path).hexdigest - end - - private - - def matching_pattern_map - @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map| - path_relative_to_upload_dir.match(path_pattern_map[:pattern]) - end - - unless @matching_pattern_map - raise "Unknown upload path pattern \"#{path}\"" - end - - @matching_pattern_map - end - - def file_uploader_model_id - matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE) - not_found_msg = <<~MSG - Could not capture project full_path from a FileUploader path: - "#{path_relative_to_upload_dir}" - MSG - raise not_found_msg unless matchd - - full_path = matchd[1] - project = Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::Project.find_by_full_path(full_path) - return unless project - - project.id - end - - # Not including a leading slash - def path_relative_to_upload_dir - upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR - base = %r{\A#{Regexp.escape(upload_dir)}/} - @path_relative_to_upload_dir ||= path.sub(base, '') - end - - def absolute_path - File.join(Gitlab.config.uploads.storage_path, path) - end - end - - # Avoid using application code - class Upload < ActiveRecord::Base - self.table_name = 'uploads' - end - - # Avoid using application code - class Appearance < ActiveRecord::Base - self.table_name = 'appearances' - end - - # Avoid using application code - class Namespace < ActiveRecord::Base - self.table_name = 'namespaces' - end - - # Avoid using application code - class Note < ActiveRecord::Base - self.table_name = 'notes' - end - - # Avoid using application code - class User < ActiveRecord::Base - self.table_name = 'users' - end - - # Since project Markdown upload paths don't contain the project ID, we have to find the - # project by its full_path. Due to MySQL/PostgreSQL differences, and historical reasons, - # the logic is somewhat complex, so I've mostly copied it in here. - class Project < ActiveRecord::Base - self.table_name = 'projects' - - def self.find_by_full_path(path) - order_sql = Arel.sql("(CASE WHEN routes.path = #{connection.quote(path)} THEN 0 ELSE 1 END)") - where_full_path_in(path).reorder(order_sql).take - end - - def self.where_full_path_in(path) - where = "(LOWER(routes.path) = LOWER(#{connection.quote(path)}))" - joins("INNER JOIN routes ON routes.source_id = projects.id AND routes.source_type = 'Project'").where(where) - end - end - end - end -end diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb deleted file mode 100644 index 3d943205783..00000000000 --- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module BackgroundMigration - # This class finds all non-hashed uploaded file paths and saves them to a - # `untracked_files_for_uploads` table. - class PrepareUntrackedUploads # rubocop:disable Metrics/ClassLength - # For bulk_queue_background_migration_jobs_by_range - include Database::MigrationHelpers - include ::Gitlab::Utils::StrongMemoize - - FIND_BATCH_SIZE = 500 - RELATIVE_UPLOAD_DIR = "uploads" - ABSOLUTE_UPLOAD_DIR = File.join( - Gitlab.config.uploads.storage_path, - RELATIVE_UPLOAD_DIR - ) - FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads' - START_WITH_ROOT_REGEX = %r{\A#{Gitlab.config.uploads.storage_path}/}.freeze - EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*" - EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*" - - # This class is used to iterate over batches of - # `untracked_files_for_uploads` rows. - class UntrackedFile < ActiveRecord::Base - include EachBatch - - self.table_name = 'untracked_files_for_uploads' - end - - def perform - ensure_temporary_tracking_table_exists - - # Since Postgres < 9.5 does not have ON CONFLICT DO NOTHING, and since - # doing inserts-if-not-exists without ON CONFLICT DO NOTHING would be - # slow, start with an empty table for Postgres < 9.5. - # That way we can do bulk inserts at ~30x the speed of individual - # inserts (~20 minutes worth of inserts at GitLab.com scale instead of - # ~10 hours). - # In all other cases, installations will get both bulk inserts and the - # ability for these jobs to retry without having to clear and reinsert. - clear_untracked_file_paths unless can_bulk_insert_and_ignore_duplicates? - - store_untracked_file_paths - - if UntrackedFile.all.empty? - drop_temp_table - else - schedule_populate_untracked_uploads_jobs - end - end - - private - - def ensure_temporary_tracking_table_exists - table_name = :untracked_files_for_uploads - - unless ActiveRecord::Base.connection.table_exists?(table_name) - UntrackedFile.connection.create_table table_name do |t| - t.string :path, limit: 600, null: false - t.index :path, unique: true - end - end - end - - def clear_untracked_file_paths - UntrackedFile.delete_all - end - - def store_untracked_file_paths - return unless Dir.exist?(ABSOLUTE_UPLOAD_DIR) - - each_file_batch(ABSOLUTE_UPLOAD_DIR, FIND_BATCH_SIZE) do |file_paths| - insert_file_paths(file_paths) - end - end - - def each_file_batch(search_dir, batch_size, &block) - cmd = build_find_command(search_dir) - - Open3.popen2(*cmd) do |stdin, stdout, status_thread| - yield_paths_in_batches(stdout, batch_size, &block) - - raise "Find command failed" unless status_thread.value.success? - end - end - - def yield_paths_in_batches(stdout, batch_size, &block) - paths = [] - - stdout.each_line("\0") do |line| - paths << line.chomp("\0").sub(START_WITH_ROOT_REGEX, '') - - if paths.size >= batch_size - yield(paths) - paths = [] - end - end - - yield(paths) if paths.any? - end - - def build_find_command(search_dir) - cmd = %W[find -L #{search_dir} - -type f - ! ( -path #{EXCLUDED_HASHED_UPLOADS_PATH} -prune ) - ! ( -path #{EXCLUDED_TMP_UPLOADS_PATH} -prune ) - -print0] - - ionice = which_ionice - cmd = %W[#{ionice} -c Idle] + cmd if ionice - - log_msg = "PrepareUntrackedUploads find command: \"#{cmd.join(' ')}\"" - Rails.logger.info log_msg # rubocop:disable Gitlab/RailsLogger - - cmd - end - - def which_ionice - Gitlab::Utils.which('ionice') - rescue StandardError - # In this case, returning false is relatively safe, - # even though it isn't very nice - false - end - - def insert_file_paths(file_paths) - sql = insert_sql(file_paths) - - ActiveRecord::Base.connection.execute(sql) - end - - def insert_sql(file_paths) - if postgresql_pre_9_5? - "INSERT INTO #{table_columns_and_values_for_insert(file_paths)};" - else - "INSERT INTO #{table_columns_and_values_for_insert(file_paths)}"\ - " ON CONFLICT DO NOTHING;" - end - end - - def table_columns_and_values_for_insert(file_paths) - values = file_paths.map do |file_path| - ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend - end.join(', ') - - "#{UntrackedFile.table_name} (path) VALUES #{values}" - end - - def can_bulk_insert_and_ignore_duplicates? - !postgresql_pre_9_5? - end - - def postgresql_pre_9_5? - strong_memoize(:postgresql_pre_9_5) do - Gitlab::Database.version.to_f < 9.5 - end - end - - def schedule_populate_untracked_uploads_jobs - bulk_queue_background_migration_jobs_by_range( - UntrackedFile, FOLLOW_UP_MIGRATION) - end - - def drop_temp_table - unless Rails.env.test? # Dropping a table intermittently breaks test cleanup - UntrackedFile.connection.drop_table(:untracked_files_for_uploads, - if_exists: true) - end - end - end - end -end diff --git a/lib/gitlab/suggestions/suggestion_set.rb b/lib/gitlab/suggestions/suggestion_set.rb index abb05ba56a7..f9a635734a3 100644 --- a/lib/gitlab/suggestions/suggestion_set.rb +++ b/lib/gitlab/suggestions/suggestion_set.rb @@ -83,7 +83,7 @@ module Gitlab end unless suggestion.appliable?(cached: false) - return _('A suggestion is not applicable.') + return suggestion.inapplicable_reason(cached: false) end unless latest_source_head?(suggestion) diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 6e703498abf..e3055ebc7bc 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -261,19 +261,10 @@ module Gitlab database: { adapter: alt_usage_data { Gitlab::Database.adapter_name }, version: alt_usage_data { Gitlab::Database.version } - }, - app_server: { type: app_server_type } + } } end - def app_server_type - Gitlab::Runtime.identify.to_s - rescue Gitlab::Runtime::IdentificationError => e - Gitlab::AppLogger.error(e.message) - Gitlab::ErrorTracking.track_exception(e) - 'unknown_app_server_type' - end - def object_store_config(component) config = alt_usage_data(fallback: nil) do Settings[component]['object_store'] diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f8892381a69..e3854d5ba38 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -420,6 +420,12 @@ msgstr "" msgid "%{labelStart}Class:%{labelEnd} %{class}" msgstr "" +msgid "%{labelStart}Crash Address:%{labelEnd} %{crash_address}" +msgstr "" + +msgid "%{labelStart}Crash State:%{labelEnd} %{stacktrace_snippet}" +msgstr "" + msgid "%{labelStart}Evidence:%{labelEnd} %{evidence}" msgstr "" @@ -1139,9 +1145,6 @@ msgstr "" msgid "A subscription will trigger a new pipeline on the default branch of this project when a pipeline successfully completes for a new tag on the %{default_branch_docs} of the subscribed project." msgstr "" -msgid "A suggestion is not applicable." -msgstr "" - msgid "A user with write access to the source branch selected this option" msgstr "" @@ -6848,9 +6851,6 @@ msgstr "" msgid "Coverage Fuzzing" msgstr "" -msgid "Crash State" -msgstr "" - msgid "Create" msgstr "" @@ -27761,6 +27761,9 @@ msgstr "" msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}" msgstr "" +msgid "ciReport|%{linkStartTag}Learn more about Coverage Fuzzing %{linkEndTag}" +msgstr "" + msgid "ciReport|%{linkStartTag}Learn more about DAST %{linkEndTag}" msgstr "" @@ -27836,6 +27839,12 @@ msgstr "" msgid "ciReport|Coverage Fuzzing" msgstr "" +msgid "ciReport|Coverage Fuzzing Title" +msgstr "" + +msgid "ciReport|Coverage fuzzing" +msgstr "" + msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually." msgstr "" diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js index 8b3853d4535..762f3c5dad1 100644 --- a/spec/frontend/ide/components/activity_bar_spec.js +++ b/spec/frontend/ide/components/activity_bar_spec.js @@ -1,15 +1,17 @@ import Vue from 'vue'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import { leftSidebarViews } from '~/ide/constants'; import ActivityBar from '~/ide/components/activity_bar.vue'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { resetStore } from '../helpers'; describe('IDE activity bar', () => { const Component = Vue.extend(ActivityBar); let vm; + let store; beforeEach(() => { + store = createStore(); + Vue.set(store.state.projects, 'abcproject', { web_url: 'testing', }); @@ -20,8 +22,6 @@ describe('IDE activity bar', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); describe('updateActivityBarView', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js index 16d0b354a30..dbb43e43c19 100644 --- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import emptyState from '~/ide/components/commit_sidebar/empty_state.vue'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; describe('IDE commit panel empty state', () => { let vm; + let store; beforeEach(() => { + store = createStore(); + const Component = Vue.extend(emptyState); Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes'); @@ -19,8 +21,6 @@ describe('IDE commit panel empty state', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders no changes text when last commit message is empty', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js index c62df4a3795..278b7bcf5aa 100644 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js @@ -1,19 +1,20 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { projectData } from 'jest/ide/mock_data'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import CommitForm from '~/ide/components/commit_sidebar/form.vue'; import { leftSidebarViews } from '~/ide/constants'; -import { resetStore } from '../../helpers'; import waitForPromises from 'helpers/wait_for_promises'; describe('IDE commit form', () => { const Component = Vue.extend(CommitForm); let vm; + let store; const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]'); beforeEach(() => { + store = createStore(); store.state.changedFiles.push('test'); store.state.currentProjectId = 'abcproject'; store.state.currentBranchId = 'master'; @@ -24,8 +25,6 @@ describe('IDE commit form', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('enables begin commit button when there are changes', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js index 45372d18965..42e0a20bc7b 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js @@ -1,14 +1,17 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue'; import { file } from '../../helpers'; import { removeWhitespace } from '../../../helpers/text_helper'; describe('Multi-file editor commit sidebar list collapsed', () => { let vm; + let store; beforeEach(() => { + store = createStore(); + const Component = Vue.extend(listCollapsed); vm = createComponentWithStore(Component, store, { diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js index 2b5664ffc4e..2107ff96e95 100644 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js @@ -1,13 +1,16 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import commitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import { file, resetStore } from '../../helpers'; +import { file } from '../../helpers'; describe('Multi-file editor commit sidebar list', () => { + let store; let vm; beforeEach(() => { + store = createStore(); + const Component = Vue.extend(commitSidebarList); vm = createComponentWithStore(Component, store, { @@ -26,8 +29,6 @@ describe('Multi-file editor commit sidebar list', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); describe('with a list of files', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js index ac80ba58056..bf61f4bbe77 100644 --- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import { resetStore } from 'jest/ide/helpers'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; describe('IDE commit sidebar radio group', () => { let vm; + let store; beforeEach(done => { + store = createStore(); + const Component = Vue.extend(radioGroup); store.state.commit.commitAction = '2'; @@ -25,8 +27,6 @@ describe('IDE commit sidebar radio group', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('uses label if present', () => { diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js index e1a432b81be..db13c90fbb9 100644 --- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import successMessage from '~/ide/components/commit_sidebar/success_message.vue'; import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { resetStore } from '../../helpers'; describe('IDE commit panel successful commit state', () => { let vm; + let store; beforeEach(() => { + store = createStore(); + const Component = Vue.extend(successMessage); vm = createComponentWithStore(Component, store, { @@ -19,8 +21,6 @@ describe('IDE commit panel successful commit state', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders last commit message when it exists', done => { diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js index e78bacadebb..4bd27d23f76 100644 --- a/spec/frontend/ide/components/file_row_extra_spec.js +++ b/spec/frontend/ide/components/file_row_extra_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { createStore } from '~/ide/stores'; import FileRowExtra from '~/ide/components/file_row_extra.vue'; -import { file, resetStore } from '../helpers'; +import { file } from '../helpers'; describe('IDE extra file row component', () => { let Component; @@ -32,7 +32,6 @@ describe('IDE extra file row component', () => { afterEach(() => { vm.$destroy(); - resetStore(vm.$store); stagedFilesCount = 0; unstagedFilesCount = 0; diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js index 21dbe18a223..5a33837fb14 100644 --- a/spec/frontend/ide/components/file_templates/bar_spec.js +++ b/spec/frontend/ide/components/file_templates/bar_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; import { createStore } from '~/ide/stores'; import Bar from '~/ide/components/file_templates/bar.vue'; -import { resetStore, file } from '../../helpers'; +import { file } from '../../helpers'; describe('IDE file templates bar component', () => { let Component; @@ -26,7 +26,6 @@ describe('IDE file templates bar component', () => { afterEach(() => { vm.$destroy(); - resetStore(vm.$store); }); describe('template type dropdown', () => { diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js index b56957e1f6d..c9ac2ac423d 100644 --- a/spec/frontend/ide/components/ide_review_spec.js +++ b/spec/frontend/ide/components/ide_review_spec.js @@ -3,7 +3,7 @@ import IdeReview from '~/ide/components/ide_review.vue'; import { createStore } from '~/ide/stores'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; import { trimText } from '../../helpers/text_helper'; -import { resetStore, file } from '../helpers'; +import { file } from '../helpers'; import { projectData } from '../mock_data'; describe('IDE review mode', () => { @@ -26,8 +26,6 @@ describe('IDE review mode', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders list of files', () => { diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js index 65cad2e7eb0..67257b40879 100644 --- a/spec/frontend/ide/components/ide_side_bar_spec.js +++ b/spec/frontend/ide/components/ide_side_bar_spec.js @@ -1,15 +1,17 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import ideSidebar from '~/ide/components/ide_side_bar.vue'; import { leftSidebarViews } from '~/ide/constants'; -import { resetStore } from '../helpers'; import { projectData } from '../mock_data'; describe('IdeSidebar', () => { let vm; + let store; beforeEach(() => { + store = createStore(); + const Component = Vue.extend(ideSidebar); store.state.currentProjectId = 'abcproject'; @@ -20,8 +22,6 @@ describe('IdeSidebar', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders a sidebar', () => { diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js index efc1d984dec..a7b07a9f0e2 100644 --- a/spec/frontend/ide/components/ide_spec.js +++ b/spec/frontend/ide/components/ide_spec.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; import { createStore } from '~/ide/stores'; import ide from '~/ide/components/ide.vue'; -import { file, resetStore } from '../helpers'; +import { file } from '../helpers'; import { projectData } from '../mock_data'; import extendStore from '~/ide/stores/extend'; @@ -41,8 +41,6 @@ describe('ide component, empty repo', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders "New file" button in empty repo', done => { @@ -63,8 +61,6 @@ describe('ide component, non-empty repo', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('shows error message when set', done => { diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js index 30f11db3153..4593ef6049b 100644 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ b/spec/frontend/ide/components/ide_tree_list_spec.js @@ -1,8 +1,8 @@ import Vue from 'vue'; import IdeTreeList from '~/ide/components/ide_tree_list.vue'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { resetStore, file } from '../helpers'; +import { file } from '../helpers'; import { projectData } from '../mock_data'; describe('IDE tree list', () => { @@ -10,6 +10,7 @@ describe('IDE tree list', () => { const normalBranchTree = [file('fileName')]; const emptyBranchTree = []; let vm; + let store; const bootstrapWithTree = (tree = normalBranchTree) => { store.state.currentProjectId = 'abcproject'; @@ -25,10 +26,12 @@ describe('IDE tree list', () => { }); }; + beforeEach(() => { + store = createStore(); + }); + afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); describe('normal branch', () => { diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js index 01f007f09c3..899daa0bf57 100644 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ b/spec/frontend/ide/components/ide_tree_spec.js @@ -1,14 +1,17 @@ import Vue from 'vue'; import IdeTree from '~/ide/components/ide_tree.vue'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { resetStore, file } from '../helpers'; +import { file } from '../helpers'; import { projectData } from '../mock_data'; describe('IdeRepoTree', () => { + let store; let vm; beforeEach(() => { + store = createStore(); + const IdeRepoTree = Vue.extend(IdeTree); store.state.currentProjectId = 'abcproject'; @@ -24,8 +27,6 @@ describe('IdeRepoTree', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders list of files', () => { diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js index 00781c16609..c6cebf36de3 100644 --- a/spec/frontend/ide/components/new_dropdown/index_spec.js +++ b/spec/frontend/ide/components/new_dropdown/index_spec.js @@ -1,13 +1,15 @@ import Vue from 'vue'; import { createComponentWithStore } from 'helpers/vue_mount_component_helper'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import newDropdown from '~/ide/components/new_dropdown/index.vue'; -import { resetStore } from '../../helpers'; describe('new dropdown component', () => { + let store; let vm; beforeEach(() => { + store = createStore(); + const component = Vue.extend(newDropdown); vm = createComponentWithStore(component, store, { @@ -30,8 +32,6 @@ describe('new dropdown component', () => { afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); }); it('renders new file, upload and new directory links', () => { diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js index a9620d26313..8caa9c2b437 100644 --- a/spec/frontend/ide/helpers.js +++ b/spec/frontend/ide/helpers.js @@ -1,25 +1,5 @@ import * as pathUtils from 'path'; import { decorateData } from '~/ide/stores/utils'; -import state from '~/ide/stores/state'; -import commitState from '~/ide/stores/modules/commit/state'; -import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; -import pipelinesState from '~/ide/stores/modules/pipelines/state'; -import branchesState from '~/ide/stores/modules/branches/state'; -import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; -import paneState from '~/ide/stores/modules/pane/state'; - -export const resetStore = store => { - const newState = { - ...state(), - commit: commitState(), - mergeRequests: mergeRequestsState(), - pipelines: pipelinesState(), - branches: branchesState(), - fileTemplates: fileTemplatesState(), - rightPane: paneState(), - }; - store.replaceState(newState); -}; export const file = (name = 'name', id = name, type = '', parent = null) => decorateData({ diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js index 4556fc9d646..e9b7faaadfe 100644 --- a/spec/frontend/ide/lib/decorations/controller_spec.js +++ b/spec/frontend/ide/lib/decorations/controller_spec.js @@ -2,14 +2,17 @@ import Editor from '~/ide/lib/editor'; import DecorationsController from '~/ide/lib/decorations/controller'; import Model from '~/ide/lib/common/model'; import { file } from '../../helpers'; +import { createStore } from '~/ide/stores'; describe('Multi-file editor library decorations controller', () => { let editorInstance; let controller; let model; + let store; beforeEach(() => { - editorInstance = Editor.create(); + store = createStore(); + editorInstance = Editor.create(store); editorInstance.createInstance(document.createElement('div')); controller = new DecorationsController(editorInstance); diff --git a/spec/frontend/ide/lib/diff/controller_spec.js b/spec/frontend/ide/lib/diff/controller_spec.js index 0b33a4c6ad6..8ee6388a760 100644 --- a/spec/frontend/ide/lib/diff/controller_spec.js +++ b/spec/frontend/ide/lib/diff/controller_spec.js @@ -4,6 +4,7 @@ import ModelManager from '~/ide/lib/common/model_manager'; import DecorationsController from '~/ide/lib/decorations/controller'; import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; import { computeDiff } from '~/ide/lib/diff/diff'; +import { createStore } from '~/ide/stores'; import { file } from '../../helpers'; describe('Multi-file editor library dirty diff controller', () => { @@ -12,9 +13,12 @@ describe('Multi-file editor library dirty diff controller', () => { let modelManager; let decorationsController; let model; + let store; beforeEach(() => { - editorInstance = Editor.create(); + store = createStore(); + + editorInstance = Editor.create(store); editorInstance.createInstance(document.createElement('div')); modelManager = new ModelManager(); diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js index 5f28309422d..529f80e6f6f 100644 --- a/spec/frontend/ide/lib/editor_spec.js +++ b/spec/frontend/ide/lib/editor_spec.js @@ -5,6 +5,7 @@ import { Selection, } from 'monaco-editor'; import Editor from '~/ide/lib/editor'; +import { createStore } from '~/ide/stores'; import { defaultEditorOptions } from '~/ide/lib/editor_options'; import { file } from '../helpers'; @@ -12,6 +13,7 @@ describe('Multi-file editor library', () => { let instance; let el; let holder; + let store; const setNodeOffsetWidth = val => { Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', { @@ -22,13 +24,14 @@ describe('Multi-file editor library', () => { }; beforeEach(() => { + store = createStore(); el = document.createElement('div'); holder = document.createElement('div'); el.appendChild(holder); document.body.appendChild(el); - instance = Editor.create(); + instance = Editor.create(store); }); afterEach(() => { @@ -44,7 +47,7 @@ describe('Multi-file editor library', () => { }); it('creates instance returns cached instance', () => { - expect(Editor.create()).toEqual(instance); + expect(Editor.create(store)).toEqual(instance); }); describe('createInstance', () => { diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js index e5c4f346459..9ca53a6b382 100644 --- a/spec/frontend/ide/stores/actions/merge_request_spec.js +++ b/spec/frontend/ide/stores/actions/merge_request_spec.js @@ -1,6 +1,6 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import store from '~/ide/stores'; +import { createStore } from '~/ide/stores'; import createFlash from '~/flash'; import { getMergeRequestData, @@ -10,7 +10,6 @@ import { } from '~/ide/stores/actions/merge_request'; import service from '~/ide/services'; import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants'; -import { resetStore } from '../../helpers'; const TEST_PROJECT = 'abcproject'; const TEST_PROJECT_ID = 17; @@ -18,9 +17,12 @@ const TEST_PROJECT_ID = 17; jest.mock('~/flash'); describe('IDE store merge request actions', () => { + let store; let mock; beforeEach(() => { + store = createStore(); + mock = new MockAdapter(axios); store.state.projects[TEST_PROJECT] = { @@ -34,7 +36,6 @@ describe('IDE store merge request actions', () => { afterEach(() => { mock.restore(); - resetStore(store); }); describe('getMergeRequestsForBranch', () => { diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js index 5a1a615c703..7c841fbbbc5 100644 --- a/spec/frontend/monitoring/components/dashboard_header_spec.js +++ b/spec/frontend/monitoring/components/dashboard_header_spec.js @@ -1,16 +1,21 @@ import { shallowMount } from '@vue/test-utils'; import { createStore } from '~/monitoring/stores'; +import * as types from '~/monitoring/stores/mutation_types'; +import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; import DashboardHeader from '~/monitoring/components/dashboard_header.vue'; import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue'; import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue'; -import { setupAllDashboards } from '../store_utils'; +import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils'; import { + environmentData, dashboardGitResponse, selfMonitoringDashboardGitResponse, dashboardHeaderProps, } from '../mock_data'; import { redirectTo } from '~/lib/utils/url_utility'; +const mockProjectPath = 'https://path/to/project'; + jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), queryToObject: jest.fn(), @@ -21,6 +26,12 @@ describe('Dashboard header', () => { let store; let wrapper; + const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); + const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlDropdownItem); + const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType); + const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' }); + const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon); + const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]'); const findCreateDashboardMenuItem = () => findActionsMenu().find('[data-testid="action-create-dashboard"]'); @@ -29,6 +40,10 @@ describe('Dashboard header', () => { const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal); const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]'); + const setSearchTerm = searchTerm => { + store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); + }; + const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(DashboardHeader, { propsData: { ...dashboardHeaderProps, ...props }, @@ -45,6 +60,113 @@ describe('Dashboard header', () => { wrapper.destroy(); }); + describe('environments dropdown', () => { + beforeEach(() => { + createShallowWrapper(); + }); + + it('shows the environments dropdown', () => { + expect(findEnvsDropdown().exists()).toBe(true); + }); + + it('renders a search input', () => { + expect(findEnvsDropdownSearch().exists()).toBe(true); + }); + + describe('when environments data is not loaded', () => { + beforeEach(() => { + setupStoreWithDashboard(store); + return wrapper.vm.$nextTick(); + }); + + it('there are no environments listed', () => { + expect(findEnvsDropdownItems().length).toBe(0); + }); + }); + + describe('when environments data is loaded', () => { + const currentDashboard = dashboardGitResponse[0].path; + + beforeEach(() => { + setupStoreWithData(store); + store.state.monitoringDashboard.projectPath = mockProjectPath; + store.state.monitoringDashboard.currentDashboard = currentDashboard; + + return wrapper.vm.$nextTick(); + }); + + it('renders dropdown items with the environment name', () => { + const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`; + + findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => { + const { name, id } = environmentData[index]; + const idParam = encodeURIComponent(id); + + expect(itemWrapper.text()).toBe(name); + expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`); + }); + }); + + // Note: This test is not working, .active does not show the active environment + // https://gitlab.com/gitlab-org/gitlab/-/issues/230615 + // eslint-disable-next-line jest/no-disabled-tests + it.skip('renders the environments dropdown with a single active element', () => { + const activeItem = findEnvsDropdownItems().wrappers.filter(itemWrapper => + itemWrapper.find('.active').exists(), + ); + + expect(activeItem.length).toBe(1); + }); + + it('filters rendered dropdown items', () => { + const searchTerm = 'production'; + const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1); + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick().then(() => { + expect(findEnvsDropdownItems().length).toBe(resultEnvs.length); + }); + }); + + it('does not filter dropdown items if search term is empty string', () => { + const searchTerm = ''; + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick(() => { + expect(findEnvsDropdownItems().length).toBe(environmentData.length); + }); + }); + + it("shows error message if search term doesn't match", () => { + const searchTerm = 'does-not-exist'; + setSearchTerm(searchTerm); + + return wrapper.vm.$nextTick(() => { + expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true); + }); + }); + + it('shows loading element when environments fetch is still loading', () => { + store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`); + + return wrapper.vm + .$nextTick() + .then(() => { + expect(findEnvsDropdownLoadingIcon().exists()).toBe(true); + }) + .then(() => { + store.commit( + `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, + environmentData, + ); + }) + .then(() => { + expect(findEnvsDropdownLoadingIcon().exists()).toBe(false); + }); + }); + }); + }); + describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => { beforeEach(() => { store.state.monitoringDashboard.projectPath = 'root/sandbox'; @@ -81,7 +203,7 @@ describe('Dashboard header', () => { }); it('is rendered if projectPath is set in store', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + store.state.monitoringDashboard.projectPath = mockProjectPath; return wrapper.vm.$nextTick().then(() => { expect(findActionsMenu().exists()).toBe(true); @@ -93,7 +215,7 @@ describe('Dashboard header', () => { }); it('contains a modal', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + store.state.monitoringDashboard.projectPath = mockProjectPath; return wrapper.vm.$nextTick().then(() => { expect(findActionsMenu().contains(CreateDashboardModal)).toBe(true); @@ -111,7 +233,7 @@ describe('Dashboard header', () => { 'when the selected dashboard can be duplicated', dashboardPath => { it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + store.state.monitoringDashboard.projectPath = mockProjectPath; setupAllDashboards(store, dashboardPath); return wrapper.vm.$nextTick().then(() => { @@ -131,7 +253,7 @@ describe('Dashboard header', () => { 'when the selected dashboard cannot be duplicated', dashboardPath => { it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => { - store.state.monitoringDashboard.projectPath = 'https://path/to/project'; + store.state.monitoringDashboard.projectPath = mockProjectPath; setupAllDashboards(store, dashboardPath); return wrapper.vm.$nextTick().then(() => { @@ -144,10 +266,8 @@ describe('Dashboard header', () => { }); describe('actions menu modals', () => { - const url = 'https://path/to/project'; - beforeEach(() => { - store.state.monitoringDashboard.projectPath = url; + store.state.monitoringDashboard.projectPath = mockProjectPath; setupAllDashboards(store); createShallowWrapper(); @@ -166,7 +286,7 @@ describe('Dashboard header', () => { }); it('"Create new dashboard" modal contains correct buttons', () => { - expect(findCreateDashboardModal().props('projectPath')).toBe(url); + expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath); }); it('"Duplicate Dashboard" opens up a modal', () => { diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 4b7f7a9ddb3..534c4c43d61 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -1,7 +1,7 @@ import { shallowMount, mount } from '@vue/test-utils'; import Tracking from '~/tracking'; import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys'; -import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; +import { GlModal, GlDeprecatedButton, GlIcon } from '@gitlab/ui'; import { objectToQuery } from '~/lib/utils/url_utility'; import VueDraggable from 'vuedraggable'; import MockAdapter from 'axios-mock-adapter'; @@ -29,7 +29,7 @@ import { setupStoreWithDataForPanelCount, setupStoreWithLinks, } from '../store_utils'; -import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data'; +import { dashboardGitResponse, storeVariables } from '../mock_data'; import { metricsDashboardViewModel, metricsDashboardPanelCount, @@ -46,12 +46,6 @@ describe('Dashboard', () => { let mock; const findDashboardHeader = () => wrapper.find(DashboardHeader); - const findEnvironmentsDropdown = () => - findDashboardHeader().find({ ref: 'monitorEnvironmentsDropdown' }); - const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem); - const setSearchTerm = searchTerm => { - store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); - }; const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(Dashboard, { @@ -90,28 +84,6 @@ describe('Dashboard', () => { } }); - describe('no metrics are available yet', () => { - beforeEach(() => { - createShallowWrapper(); - }); - - it('shows the environment selector', () => { - expect(findEnvironmentsDropdown().exists()).toBe(true); - }); - }); - - describe('no data found', () => { - beforeEach(() => { - createShallowWrapper(); - - return wrapper.vm.$nextTick(); - }); - - it('shows the environment selector dropdown', () => { - expect(findEnvironmentsDropdown().exists()).toBe(true); - }); - }); - describe('request information to the server', () => { it('calls to set time range and fetch data', () => { createShallowWrapper({ hasMetrics: true }); @@ -149,17 +121,14 @@ describe('Dashboard', () => { }); it('fetches the metrics data with proper time window', () => { - jest.spyOn(store, 'dispatch'); - createMountedWrapper({ hasMetrics: true }); - store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); - return wrapper.vm.$nextTick().then(() => { - expect(store.dispatch).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined); + expect(store.dispatch).toHaveBeenCalledWith( + 'monitoringDashboard/setTimeRange', + expect.objectContaining({ duration: { seconds: 28800 } }), + ); }); }); }); @@ -500,21 +469,6 @@ describe('Dashboard', () => { return wrapper.vm.$nextTick(); }); - it('renders the environments dropdown with a number of environments', () => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length); - - findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => { - const anchorEl = itemWrapper.find('a'); - if (anchorEl.exists()) { - const href = anchorEl.attributes('href'); - const currentDashboard = encodeURIComponent(dashboardGitResponse[0].path); - const environmentId = encodeURIComponent(environmentData[index].id); - const url = `${TEST_HOST}/-/metrics/${currentDashboard}?environment=${environmentId}`; - expect(href).toBe(url); - } - }); - }); - it('it does not show loading icons in any group', () => { setupStoreWithData(store); @@ -524,16 +478,6 @@ describe('Dashboard', () => { }); }); }); - - // Note: This test is not working, .active does not show the active environment - // eslint-disable-next-line jest/no-disabled-tests - it.skip('renders the environments dropdown with a single active element', () => { - const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper => - itemWrapper.find('.active').exists(), - ); - - expect(activeItem.length).toBe(1); - }); }); describe('star dashboards', () => { @@ -615,16 +559,6 @@ describe('Dashboard', () => { }); }); - it('hides the environments dropdown list when there is no environments', () => { - createMountedWrapper({ hasMetrics: true }); - - setupStoreWithDashboard(store); - - return wrapper.vm.$nextTick().then(() => { - expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); - }); - }); - it('renders the datetimepicker dropdown', () => { createMountedWrapper({ hasMetrics: true }); @@ -811,100 +745,6 @@ describe('Dashboard', () => { }); }); - describe('searchable environments dropdown', () => { - beforeEach(() => { - createMountedWrapper({ hasMetrics: true }, { attachToDocument: true }); - - setupStoreWithData(store); - - return wrapper.vm.$nextTick(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a search input', () => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownSearch' }) - .exists(), - ).toBe(true); - }); - - it('renders dropdown items', () => { - findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => { - const anchorEl = itemWrapper.find('a'); - if (anchorEl.exists()) { - expect(anchorEl.text()).toBe(environmentData[index].name); - } - }); - }); - - it('filters rendered dropdown items', () => { - const searchTerm = 'production'; - const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1); - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick().then(() => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length); - }); - }); - - it('does not filter dropdown items if search term is empty string', () => { - const searchTerm = ''; - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick(() => { - expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length); - }); - }); - - it("shows error message if search term doesn't match", () => { - const searchTerm = 'does-not-exist'; - setSearchTerm(searchTerm); - - return wrapper.vm.$nextTick(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownMsg' }) - .isVisible(), - ).toBe(true); - }); - }); - - it('shows loading element when environments fetch is still loading', () => { - store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`); - - return wrapper.vm - .$nextTick() - .then(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownLoading' }) - .exists(), - ).toBe(true); - }) - .then(() => { - store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); - }) - .then(() => { - expect( - wrapper - .find(DashboardHeader) - .find({ ref: 'monitorEnvironmentsDropdownLoading' }) - .exists(), - ).toBe(false); - }); - }); - }); - describe('drag and drop function', () => { const findDraggables = () => wrapper.findAll(VueDraggable); const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled')); diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb deleted file mode 100644 index 6a25e8e2784..00000000000 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb +++ /dev/null @@ -1,263 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile do - include MigrationsHelpers::TrackUntrackedUploadsHelpers - - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:uploads) { table(:uploads) } - - before(:all) do - ensure_temporary_tracking_table_exists - end - - describe '#upload_path' do - def assert_upload_path(file_path, expected_upload_path) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.upload_path).to eq(expected_upload_path) - end - - context 'for an appearance logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') - end - end - - context 'for an appearance header_logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') - end - end - - context 'for a user avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') - end - end - - context 'for a group avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') - end - end - - context 'for a project avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the file path relative to the project directory in uploads' do - project = create_project - random_hex = SecureRandom.hex - - assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") - end - end - end - - describe '#uploader' do - def assert_uploader(file_path, expected_uploader) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.uploader).to eq(expected_uploader) - end - - context 'for an appearance logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for an appearance header_logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') - end - end - - context 'for a user avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a group avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns FileUploader as a string' do - project = create_project - - assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') - end - end - end - - describe '#model_type' do - def assert_model_type(file_path, expected_model_type) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_type).to eq(expected_model_type) - end - - context 'for an appearance logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for an appearance header_logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns Note as a string' do - assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') - end - end - - context 'for a user avatar file path' do - it 'returns User as a string' do - assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') - end - end - - context 'for a group avatar file path' do - it 'returns Namespace as a string' do - assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') - end - end - - context 'for a project avatar file path' do - it 'returns Project as a string' do - assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns Project as a string' do - project = create_project - - assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') - end - end - end - - describe '#model_id' do - def assert_model_id(file_path, expected_model_id) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_id).to eq(expected_model_id) - end - - context 'for an appearance logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) - end - end - - context 'for an appearance header_logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) - end - end - - context 'for a user avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a group avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the ID as a string' do - project = create_project - - assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) - end - end - end - - describe '#file_size' do - context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: true) } - let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - - context 'for a project avatar file path' do - let(:project) { create_project(avatar: true) } - let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create_project } - let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } - - before do - add_markdown_attachment(project) - end - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(1062) - end - end - end - - def create_untracked_file(path_relative_to_upload_dir) - described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") - end -end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb deleted file mode 100644 index 787cc54e79a..00000000000 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ /dev/null @@ -1,254 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::PopulateUntrackedUploads do - include MigrationsHelpers::TrackUntrackedUploadsHelpers - - subject { described_class.new } - - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:notes) { table(:notes) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } - let!(:uploads) { table(:uploads) } - let!(:users) { table(:users) } - - before do - ensure_temporary_tracking_table_exists - uploads.delete_all - end - - context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } - let!(:user1) { create_user(avatar: true) } - let!(:user2) { create_user(avatar: true) } - let!(:project1) { create_project(avatar: true) } - let!(:project2) { create_project(avatar: true) } - - before do - add_markdown_attachment(project1) - add_markdown_attachment(project2) - - # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) - untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) - untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) - untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) - untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) - untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) - untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") - untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") - - # Untrack 4 files - get_uploads(user2, 'User').delete_all - get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload - get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all - end - - it 'adds untracked files to the uploads table' do - expect do - subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) - end.to change { uploads.count }.from(4).to(8) - - expect(get_uploads(user2, 'User').count).to eq(1) - expect(get_uploads(project2, 'Project').count).to eq(2) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) - end - - it 'deletes rows after processing them' do - expect(subject).to receive(:drop_temp_table_if_finished) # Don't drop the table so we can look at it - - expect do - subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { untracked_files_for_uploads.count }.from(8).to(0) - end - - it 'does not create duplicate uploads of already tracked files' do - subject.perform(1, untracked_files_for_uploads.last.id) - - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) - end - - it 'uses the start and end batch ids [only 1st half]' do - ids = untracked_files_for_uploads.all.order(:id).pluck(:id) - start_id = ids[0] - end_id = ids[3] - - expect do - subject.perform(start_id, end_id) - end.to change { uploads.count }.from(4).to(6) - - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(user2, 'User').count).to eq(1) - expect(get_uploads(appearance, 'Appearance').count).to eq(2) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(project2, 'Project').count).to eq(0) - - # Only 4 have been either confirmed or added to uploads - expect(untracked_files_for_uploads.count).to eq(4) - end - - it 'uses the start and end batch ids [only 2nd half]' do - ids = untracked_files_for_uploads.all.order(:id).pluck(:id) - start_id = ids[4] - end_id = ids[7] - - expect do - subject.perform(start_id, end_id) - end.to change { uploads.count }.from(4).to(6) - - expect(get_uploads(user1, 'User').count).to eq(1) - expect(get_uploads(user2, 'User').count).to eq(0) - expect(get_uploads(appearance, 'Appearance').count).to eq(1) - expect(get_uploads(project1, 'Project').count).to eq(2) - expect(get_uploads(project2, 'Project').count).to eq(2) - - # Only 4 have been either confirmed or added to uploads - expect(untracked_files_for_uploads.count).to eq(4) - end - - it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do - subject.perform(1, untracked_files_for_uploads.last.id - 1) - - expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy - end - - it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do - expect(subject).to receive(:drop_temp_table_if_finished) - - subject.perform(1, untracked_files_for_uploads.last.id) - end - - it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") - expect(untracked_files_for_uploads.count).to eq(9) - expect(uploads.count).to eq(4) - - subject.perform(1, untracked_files_for_uploads.last.id) - - expect(untracked_files_for_uploads.count).to eq(1) - expect(uploads.count).to eq(8) - end - - it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" - untracked_files_for_uploads.create!(path: bad_path) - - expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) - - subject.perform(1, untracked_files_for_uploads.last.id) - end - end - - context 'with no untracked files' do - it 'does not add to the uploads table (and does not raise error)' do - expect do - subject.perform(1, 1000) - end.not_to change { uploads.count }.from(0) - end - end - - describe 'upload outcomes for each path pattern' do - shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } - let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } - - before do - model_uploads.delete_all - end - - it 'creates an Upload record' do - expect do - subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model_uploads.count }.from(0).to(1) - - expect(model_uploads.first.attributes).to include(expected_upload_attrs) - end - end - - context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: true) } - let(:model_uploads) { get_uploads(model, 'Appearance') } - - it_behaves_like 'non_markdown_file' - end - - context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: true) } - let(:model_uploads) { get_uploads(model, 'Appearance') } - - it_behaves_like 'non_markdown_file' - end - - context 'for a pre-Markdown Note attachment file path' do - let(:model) { create_note(attachment: true) } - let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } - let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } - - before do - get_uploads(model, 'Note').delete_all - end - - # Can't use the shared example because Note doesn't have an `uploads` association - it 'creates an Upload record' do - expect do - subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { get_uploads(model, 'Note').count }.from(0).to(1) - - expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) - end - end - - context 'for a user avatar file path' do - let(:model) { create_user(avatar: true) } - let(:model_uploads) { get_uploads(model, 'User') } - - it_behaves_like 'non_markdown_file' - end - - context 'for a group avatar file path' do - let(:model) { create_group(avatar: true) } - let(:model_uploads) { get_uploads(model, 'Namespace') } - - it_behaves_like 'non_markdown_file' - end - - context 'for a project avatar file path' do - let(:model) { create_project(avatar: true) } - let(:model_uploads) { get_uploads(model, 'Project') } - - it_behaves_like 'non_markdown_file' - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create_project } - - before do - # Upload the file - add_markdown_attachment(model) - - # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") - - # Save the expected upload attributes - @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') - - # Untrack the file - get_uploads(model, 'Project').delete_all - end - - it 'creates an Upload record' do - expect do - subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { get_uploads(model, 'Project').count }.from(0).to(1) - - expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) - end - end - end -end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb deleted file mode 100644 index 9b01407dc8b..00000000000 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ /dev/null @@ -1,159 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. -RSpec.describe Gitlab::BackgroundMigration::PrepareUntrackedUploads do - include MigrationsHelpers::TrackUntrackedUploadsHelpers - - let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } - let!(:appearances) { table(:appearances) } - let!(:namespaces) { table(:namespaces) } - let!(:projects) { table(:projects) } - let!(:routes) { table(:routes) } - let!(:uploads) { table(:uploads) } - let!(:users) { table(:users) } - - around do |example| - # Especially important so the follow-up migration does not get run - Sidekiq::Testing.fake! do - example.run - end - end - - shared_examples 'prepares the untracked_files_for_uploads table' do - context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } - let!(:user) { create_user(avatar: true) } - let!(:project1) { create_project(avatar: true) } - let(:project2) { create_project } # instantiate after enabling hashed_storage - - before do - # Markdown upload before enabling hashed_storage - add_markdown_attachment(project1) - - # Markdown upload after enabling hashed_storage - add_markdown_attachment(project2, hashed_storage: true) - end - - it 'has a path field long enough for really long paths' do - described_class.new.perform - - component = 'a' * 255 - - long_path = [ - 'uploads', - component, # project.full_path - component # filename - ].flatten.join('/') - - record = untracked_files_for_uploads.create!(path: long_path) - expect(record.reload.path.size).to eq(519) - end - - it 'adds unhashed files to the untracked_files_for_uploads table' do - described_class.new.perform - - expect(untracked_files_for_uploads.count).to eq(5) - end - - it 'adds files with paths relative to CarrierWave.root' do - described_class.new.perform - untracked_files_for_uploads.all.each do |file| - expect(file.path.start_with?('uploads/')).to be_truthy - end - end - - it 'does not add hashed files to the untracked_files_for_uploads table' do - described_class.new.perform - - hashed_file_path = get_uploads(project2, 'Project').find_by(uploader: 'FileUploader').path - expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey - end - - it 'correctly schedules the follow-up background migration jobs' do - described_class.new.perform - - ids = described_class::UntrackedFile.all.order(:id).pluck(:id) - expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(ids.first, ids.last) - expect(BackgroundMigrationWorker.jobs.size).to eq(1) - end - - # E.g. from a previous failed run of this background migration - context 'when there is existing data in untracked_files_for_uploads' do - before do - described_class.new.perform - end - - it 'does not error or produce duplicates of existing data' do - expect do - described_class.new.perform - end.not_to change { untracked_files_for_uploads.count }.from(5) - end - end - - # E.g. The installation is in use at the time of migration, and someone has - # just uploaded a file - context 'when there are files in /uploads/tmp' do - let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } - - before do - FileUtils.mkdir(File.dirname(tmp_file)) - FileUtils.touch(tmp_file) - end - - after do - FileUtils.rm(tmp_file) - end - - it 'does not add files from /uploads/tmp' do - described_class.new.perform - - expect(untracked_files_for_uploads.count).to eq(5) - end - end - - context 'when the last batch size exactly matches the max batch size' do - it 'does not raise error' do - stub_const("#{described_class}::FIND_BATCH_SIZE", 5) - - expect do - described_class.new.perform - end.not_to raise_error - - expect(untracked_files_for_uploads.count).to eq(5) - end - end - end - end - - # If running on Postgres 9.2 (like on CI), this whole context is skipped - # since we're unable to use ON CONFLICT DO NOTHING or IGNORE. - context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE", if: described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) do - it_behaves_like 'prepares the untracked_files_for_uploads table' - end - - # If running on Postgres 9.2 (like on CI), the stubbed method has no effect. - # - # If running on Postgres 9.5+ or MySQL, then this context effectively tests - # the bulk insert functionality without ON CONFLICT DO NOTHING or IGNORE. - context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do - before do - allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true) - end - - it_behaves_like 'prepares the untracked_files_for_uploads table' - end - - # Very new or lightly-used installations that are running this migration - # may not have an upload directory because they have no uploads. - context 'when no files were ever uploaded' do - it 'deletes the `untracked_files_for_uploads` table (and does not raise error)' do - background_migration = described_class.new - - expect(background_migration).to receive(:drop_temp_table) - - background_migration.perform - end - end -end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index f733c066127..a340e69df65 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -574,10 +574,8 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do subject { described_class.components_usage_data } it 'gathers basic components usage data' do - stub_runtime(:puma) stub_application_setting(container_registry_vendor: 'gitlab', container_registry_version: 'x.y.z') - expect(subject[:app_server][:type]).to eq('puma') expect(subject[:gitlab_pages][:enabled]).to eq(Gitlab.config.pages.enabled) expect(subject[:gitlab_pages][:version]).to eq(Gitlab::Pages::VERSION) expect(subject[:git][:version]).to eq(Gitlab::Git.version) @@ -591,32 +589,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do expect(subject[:container_registry][:vendor]).to eq('gitlab') expect(subject[:container_registry][:version]).to eq('x.y.z') end - - def stub_runtime(runtime) - allow(Gitlab::Runtime).to receive(:identify).and_return(runtime) - end - end - - describe '.app_server_type' do - subject { described_class.app_server_type } - - it 'successfully identifies runtime and returns the identifier' do - expect(Gitlab::Runtime).to receive(:identify).and_return(:runtime_identifier) - - is_expected.to eq('runtime_identifier') - end - - context 'when runtime is not identified' do - let(:exception) { Gitlab::Runtime::IdentificationError.new('exception message from runtime identify') } - - it 'logs the exception and returns unknown app server type' do - expect(Gitlab::Runtime).to receive(:identify).and_raise(exception) - - expect(Gitlab::AppLogger).to receive(:error).with(exception.message) - expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception) - expect(subject).to eq('unknown_app_server_type') - end - end end describe '.object_store_config' do diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index 34d3c54d700..78a2688ac5e 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -74,7 +74,7 @@ RSpec.describe API::Suggestions do put api(url, user) expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' }) + expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." }) end end @@ -133,7 +133,7 @@ RSpec.describe API::Suggestions do params: { ids: [suggestion.id, unappliable_suggestion.id] } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response).to eq({ 'message' => 'A suggestion is not applicable.' }) + expect(json_response).to eq({ 'message' => "Can't apply as this line was changed in a more recent version." }) end end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index aa9caf35987..d8ade0fbbda 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -609,40 +609,16 @@ RSpec.describe Suggestions::ApplyService do end end - context 'suggestion is eligible to be outdated' do + context 'suggestion is not appliable' do + let(:inapplicable_reason) { "Can't apply this suggestion." } + it 'returns error message' do - expect(suggestion).to receive(:outdated?) { true } + expect(suggestion).to receive(:appliable?).and_return(false) + expect(suggestion).to receive(:inapplicable_reason).and_return(inapplicable_reason) result = apply_service.new(user, suggestion).execute - expect(result).to eq(message: 'A suggestion is not applicable.', - status: :error) - end - end - - context 'note is outdated' do - before do - allow(diff_note).to receive(:active?) { false } - end - - it 'returns error message' do - result = apply_service.new(user, suggestion).execute - - expect(result).to eq(message: 'A suggestion is not applicable.', - status: :error) - end - end - - context 'suggestion was already applied' do - before do - suggestion.update!(applied: true, commit_id: 'sha') - end - - it 'returns error message' do - result = apply_service.new(user, suggestion).execute - - expect(result).to eq(message: 'A suggestion is not applicable.', - status: :error) + expect(result).to eq(message: inapplicable_reason, status: :error) end end diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb deleted file mode 100644 index 656be3b6d4d..00000000000 --- a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,130 +0,0 @@ -# frozen_string_literal: true - -module MigrationsHelpers - module TrackUntrackedUploadsHelpers - PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') - UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') - SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') - UPLOAD_FILENAME = 'image.png'.freeze - FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') - FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze - - def create_or_update_appearance(logo: false, header_logo: false) - appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) - - add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo - add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo - - appearance - end - - def create_group(avatar: false) - index = unique_index(:group) - group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) - - add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar - - group - end - - def create_note(attachment: false) - note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) - - add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment - - note - end - - def create_project(avatar: false) - group = create_group - project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) - routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works - - add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar - - project - end - - def create_user(avatar: false) - user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) - - add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar - - user - end - - def unique_index(name = :unnamed) - @unique_index ||= {} - @unique_index[name] ||= 0 - @unique_index[name] += 1 - end - - def add_upload(model, model_type, attachment_type, uploader) - file_path = upload_file_path(model, model_type, attachment_type) - path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') - create_file(file_path) - - uploads.create!( - size: 1062, - path: path_relative_to_public, - model_id: model.id, - model_type: model_type == 'Group' ? 'Namespace' : model_type, - uploader: uploader, - checksum: FIXTURE_CHECKSUM - ) - end - - def add_markdown_attachment(project, hashed_storage: false) - project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) - attachment_dir = File.join(project_dir, SecureRandom.hex) - attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) - project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') - create_file(attachment_file_path) - - uploads.create!( - size: 1062, - path: project_attachment_path_relative_to_project, - model_id: project.id, - model_type: 'Project', - uploader: 'FileUploader', - checksum: FIXTURE_CHECKSUM - ) - end - - def legacy_project_uploads_dir(project) - namespace = namespaces.find_by(id: project.namespace_id) - File.join(UPLOADS_DIR, namespace.path, project.path) - end - - def hashed_project_uploads_dir(project) - File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') - end - - def upload_file_path(model, model_type, attachment_type) - dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) - File.join(dir, UPLOAD_FILENAME) - end - - def upload_dir(model_type, attachment_type) - File.join(SYSTEM_DIR, model_type, attachment_type) - end - - def create_file(path) - File.delete(path) if File.exist?(path) - FileUtils.mkdir_p(File.dirname(path)) - FileUtils.cp(FIXTURE_FILE_PATH, path) - end - - def get_uploads(model, model_type) - uploads.where(model_type: model_type, model_id: model.id) - end - - def get_full_path(project) - routes.find_by(source_id: project.id, source_type: 'Project').path - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - end -end