diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc305a18156..0ef5d830548 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -5,6 +5,7 @@ stages: - build-images - fixtures - lint + - test-frontend - test - post-test - review diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml index 4fad0e5ce64..23a1563a756 100644 --- a/.gitlab/ci/frontend.gitlab-ci.yml +++ b/.gitlab/ci/frontend.gitlab-ci.yml @@ -1,3 +1,11 @@ +.with-fixtures-needs: + needs: + - "rspec-all frontend_fixture" + +.with-graphql-schema-dump-needs: + needs: + - "graphql-schema-dump" + .compile-assets-base: extends: - .default-retry @@ -83,7 +91,6 @@ update-assets-compile-production-cache: - .update-cache-base - .assets-compile-cache-push - .shared:rules:update-cache - stage: prepare artifacts: {} # This job's purpose is only to update the cache. update-assets-compile-test-cache: @@ -92,7 +99,6 @@ update-assets-compile-test-cache: - .update-cache-base - .assets-compile-cache-push - .shared:rules:update-cache - stage: prepare artifacts: {} # This job's purpose is only to update the cache. update-storybook-yarn-cache: @@ -136,8 +142,14 @@ retrieve-frontend-fixtures: - .default-before_script - .ruby-cache - .use-pg14 + - .repo-from-artifacts stage: fixtures - needs: ["setup-test-env", "retrieve-tests-metadata", "retrieve-frontend-fixtures"] + needs: + - "setup-test-env" + - "retrieve-tests-metadata" + - "retrieve-frontend-fixtures" + # it's ok to wait for the repo artifact as we're waiting for setup-test-env (which takes longer than clone-gitlab-repo) anyway + - !reference [.repo-from-artifacts, needs] variables: # Don't add `CRYSTALBALL: "false"` here as we're enabling Crystalball for scheduled pipelines (in `.gitlab-ci.yml`), so that we get coverage data # for the `frontend fixture RSpec files` that will be added to the Crystalball mapping in `update-tests-metadata`. @@ -180,18 +192,21 @@ upload-frontend-fixtures: variables: SETUP_DB: "false" extends: - - .frontend-fixtures-base + - .default-retry + - .default-before_script + - .repo-from-artifacts - .frontend:rules:upload-frontend-fixtures stage: fixtures - needs: ["rspec-all frontend_fixture"] + needs: + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] script: - - source scripts/utils.sh - source scripts/gitlab_component_helpers.sh - export_fixtures_sha_for_upload - 'fixtures_archive_doesnt_exist || { echoinfo "INFO: Exiting early as package exists."; exit 0; }' - run_timed_command "create_fixtures_package" - run_timed_command "upload_fixtures_package" - artifacts: {} graphql-schema-dump: variables: @@ -225,18 +240,7 @@ graphql-schema-dump: before_script: - !reference [.default-before_script, before_script] - yarn_install_script - stage: test - -.vue3: - variables: - VUE_VERSION: 3 - NODE_OPTIONS: --max-old-space-size=7680 - allow_failure: true - -.jest-base: - extends: .frontend-test-base - script: - - run_timed_command "yarn jest:ci:without-fixtures" + stage: test-frontend jest-build-cache: extends: @@ -260,37 +264,26 @@ jest-build-cache: # they exit with 1, so as not to break master and other pipelines. exit_codes: 1 +.vue3: + variables: + VUE_VERSION: 3 + NODE_OPTIONS: --max-old-space-size=7680 + allow_failure: true + +.with-jest-build-cache-vue3-needs: + needs: + - job: jest-build-cache-vue3 + optional: true + jest-build-cache-vue3: extends: - jest-build-cache - .frontend:rules:jest-vue3 - .vue3 -jest-with-fixtures: - extends: - - .jest-base - - .frontend:rules:jest - needs: - - "rspec-all frontend_fixture" - - job: jest-build-cache - optional: true - artifacts: - name: coverage-frontend - expire_in: 31d - when: always - paths: - - coverage-frontend/ - - junit_jest.xml - - tmp/tests/frontend/ - reports: - junit: junit_jest.xml - parallel: 2 - script: - - run_timed_command "yarn jest:ci:with-fixtures" - jest: extends: - - .jest-base + - .frontend-test-base - .frontend:rules:jest needs: - job: jest-build-cache @@ -306,16 +299,22 @@ jest: reports: junit: junit_jest.xml parallel: 11 + script: + - run_timed_command "yarn jest:ci:without-fixtures" -jest-with-fixtures vue3: +jest-with-fixtures: extends: - - jest-with-fixtures - - .frontend:rules:jest-vue3 - - .vue3 + - jest + - .repo-from-artifacts + - .frontend:rules:jest needs: - - "rspec-all frontend_fixture" - - job: jest-build-cache-vue3 - optional: true + - !reference [jest, needs] + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] + parallel: 2 + script: + - run_timed_command "yarn jest:ci:with-fixtures" jest vue3: extends: @@ -323,8 +322,18 @@ jest vue3: - .frontend:rules:jest-vue3 - .vue3 needs: - - job: jest-build-cache-vue3 - optional: true + - !reference [.with-jest-build-cache-vue3-needs, needs] + +jest-with-fixtures vue3: + extends: + - jest-with-fixtures + - .frontend:rules:jest-vue3 + - .vue3 + needs: + - !reference ["jest vue3", needs] + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] jest predictive: extends: @@ -349,16 +358,25 @@ jest-with-fixtures predictive: jest-integration: extends: - .frontend-test-base + - .repo-from-artifacts - .frontend:rules:jest-integration script: - run_timed_command "yarn jest:integration --ci" - needs: ["rspec-all frontend_fixture", "graphql-schema-dump"] + needs: + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] + - !reference [.with-graphql-schema-dump-needs, needs] jest-snapshot-vue3: extends: - - .jest-base + - .frontend-test-base + - .repo-from-artifacts - .frontend:rules:jest-snapshot-vue3 - needs: ["rspec-all frontend_fixture"] + needs: + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] variables: VUE_VERSION: 3 JEST_REPORT: jest-test-report.json @@ -381,7 +399,6 @@ jest-snapshot-vue3: echo 'All snapshot tests passed! Exiting 0...' exit 0 fi - artifacts: name: snapshot_tests expire_in: 31d @@ -395,8 +412,10 @@ coverage-frontend: - .default-retry - .default-utils-before_script - .yarn-cache + - .repo-from-artifacts - .frontend:rules:coverage-frontend needs: + - !reference [.repo-from-artifacts, needs] - job: "jest" optional: true - job: "jest-with-fixtures" @@ -427,9 +446,9 @@ webpack-dev-server: - .default-retry - .default-utils-before_script - .yarn-cache + - .repo-from-artifacts - .frontend:rules:default-frontend-jobs - stage: test - needs: [] + stage: test-frontend variables: WEBPACK_MEMORY_TEST: "true" WEBPACK_VENDOR_DLL: "true" @@ -443,44 +462,24 @@ webpack-dev-server: paths: - webpack-dev-server.json -bundle-size-review: - extends: - - .default-retry - - .default-utils-before_script - - .assets-compile-cache - - .frontend:rules:bundle-size-review - image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:danger - stage: test - needs: [] - script: - - yarn_install_script - - scripts/bundle_size_review - artifacts: - when: always - name: bundle-size-review - expire_in: 31d - paths: - - bundle-size-review/ - -.compile-storybook-base: +compile-storybook: extends: - .frontend-test-base - .storybook-yarn-cache - script: - - yarn_install_script_storybook - - run_timed_command "yarn run storybook:build" - needs: ["graphql-schema-dump"] - -compile-storybook: - extends: - - .compile-storybook-base + - .repo-from-artifacts - .frontend:rules:compile-storybook + stage: pages needs: - - !reference [.compile-storybook-base, needs] - - job: "rspec-all frontend_fixture" + # it's ok to wait for the repo artifact as we're waiting for the fixtures (which wait for the repo artifact) anyway + - !reference [.repo-from-artifacts, needs] + - !reference [.with-fixtures-needs, needs] + - !reference [.with-graphql-schema-dump-needs, needs] artifacts: name: storybook expire_in: 31d when: always paths: - storybook/public + script: + - yarn_install_script_storybook + - run_timed_command "yarn run storybook:build" diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 73a0e7926ec..a90ae34622f 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -28,7 +28,8 @@ needs: # If the job extending this also defines `needs`, make sure to update # its `needs` to include `clone-gitlab-repo` because it'll be overridden. - - clone-gitlab-repo + - job: clone-gitlab-repo + optional: true # Optional so easier to switch in between .production: variables: @@ -321,7 +322,7 @@ .ai-gateway-services: services: - - name: registry.gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/model-gateway:v1.7.0 + - name: registry.gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/model-gateway:v1.8.0 alias: ai-gateway .use-pg13: diff --git a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml index 6453936b6cb..15d50bb88c9 100644 --- a/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml +++ b/.gitlab/ci/includes/gitlab-com/danger-review.gitlab-ci.yml @@ -2,6 +2,7 @@ include: - component: ${CI_SERVER_FQDN}/gitlab-org/components/danger-review/danger-review@1.4.1 inputs: job_image: "${DEFAULT_CI_IMAGE}" + job_stage: "preflight" # By default DANGER_DANGERFILE_PREFIX is not defined but allows JiHu to # use a different prefix. # See https://jihulab.com/gitlab-cn/gitlab/-/blob/main-jh/jh/.gitlab-ci.yml diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml index ad8c3047396..e2132d3eeec 100644 --- a/.gitlab/ci/preflight.gitlab-ci.yml +++ b/.gitlab/ci/preflight.gitlab-ci.yml @@ -115,3 +115,21 @@ pipeline-tier-3: extends: - .pipeline-tier-base - .preflight:rules:pipeline-tier-3 + +bundle-size-review: + extends: + - .default-retry + - .default-utils-before_script + - .assets-compile-cache + - .repo-from-artifacts + - .frontend:rules:bundle-size-review + stage: preflight + script: + - yarn_install_script + - scripts/bundle_size_review + artifacts: + when: always + name: bundle-size-review + expire_in: 31d + paths: + - bundle-size-review/ diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 9468d958ea1..2dea48c8c3e 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -103,6 +103,7 @@ qa:metadata-lint: extends: - .qa-job-base - .qa:rules:metadata-lint + stage: lint variables: QA_EXPORT_TEST_METRICS: "false" # Disable warnings in browserslist which can break on backports diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index 1d0adfa2ade..b5153ae668c 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -1367,7 +1367,6 @@ fail-pipeline-early: stage: test needs: - !reference [.rspec-base-needs, needs] - - job: "compile-test-assets" - job: "detect-previous-failed-tests" script: - !reference [.base-script, script] diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml index e9c976db7fc..9221edba954 100644 --- a/.gitlab/ci/rails/shared.gitlab-ci.yml +++ b/.gitlab/ci/rails/shared.gitlab-ci.yml @@ -68,10 +68,10 @@ include: .rspec-base-needs: needs: - - job: "clone-gitlab-repo" - optional: true # Optional so easier to switch in between + - !reference [.repo-from-artifacts, needs] - job: "setup-test-env" - job: "retrieve-tests-metadata" + - job: "compile-test-assets" .rspec-base: extends: @@ -89,7 +89,6 @@ include: EVENT_PROF: "sql.active_record" needs: - !reference [.rspec-base-needs, needs] - - job: "compile-test-assets" - job: "detect-tests" optional: true script: diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml index 0acec43ca31..0ebe3c58619 100644 --- a/.gitlab/ci/setup.gitlab-ci.yml +++ b/.gitlab/ci/setup.gitlab-ci.yml @@ -91,7 +91,7 @@ verify-tests-yml: extends: - .setup:rules:verify-tests-yml image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.20 - stage: test + stage: preflight needs: [] script: - source scripts/utils.sh diff --git a/.rubocop_todo/gitlab/strong_memoize_attr.yml b/.rubocop_todo/gitlab/strong_memoize_attr.yml index 812adf43627..8dd0246cea4 100644 --- a/.rubocop_todo/gitlab/strong_memoize_attr.yml +++ b/.rubocop_todo/gitlab/strong_memoize_attr.yml @@ -593,7 +593,6 @@ Gitlab/StrongMemoizeAttr: - 'lib/gitlab/tracking/destinations/snowplow_micro.rb' - 'lib/gitlab/usage_data.rb' - 'lib/gitlab/web_hooks/rate_limiter.rb' - - 'lib/gitlab/web_ide/config/entry/terminal.rb' - 'lib/gitlab/webpack/graphql_known_operations.rb' - 'lib/gitlab/webpack/manifest.rb' - 'lib/gitlab/wiki_pages/front_matter_parser.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 0f9db924372..ed220197203 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -3696,8 +3696,6 @@ Layout/LineLength: - 'spec/lib/gitlab/utils/measuring_spec.rb' - 'spec/lib/gitlab/utils/nokogiri_spec.rb' - 'spec/lib/gitlab/utils/usage_data_spec.rb' - - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb' - - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' - 'spec/lib/gitlab/webpack/manifest_spec.rb' - 'spec/lib/gitlab/word_diff/parser_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb' diff --git a/.rubocop_todo/lint/redundant_cop_disable_directive.yml b/.rubocop_todo/lint/redundant_cop_disable_directive.yml index 35e86a5aaec..844fa07143d 100644 --- a/.rubocop_todo/lint/redundant_cop_disable_directive.yml +++ b/.rubocop_todo/lint/redundant_cop_disable_directive.yml @@ -344,7 +344,6 @@ Lint/RedundantCopDisableDirective: - 'lib/gitlab/usage_data.rb' - 'lib/gitlab/usage_data_queries.rb' - 'lib/gitlab/verify/ci_secure_files.rb' - - 'lib/gitlab/web_ide/extensions_marketplace.rb' - 'lib/gitlab/x509/signature.rb' - 'lib/tasks/gitlab/cleanup.rake' - 'lib/tasks/gitlab/seed/group_seed.rake' diff --git a/.rubocop_todo/lint/unused_method_argument.yml b/.rubocop_todo/lint/unused_method_argument.yml index 863caa4203e..7cfa1b2c328 100644 --- a/.rubocop_todo/lint/unused_method_argument.yml +++ b/.rubocop_todo/lint/unused_method_argument.yml @@ -472,7 +472,6 @@ Lint/UnusedMethodArgument: - 'lib/gitlab/utils/usage_data.rb' - 'lib/gitlab/verify/batch_verifier.rb' - 'lib/gitlab/view/presenter/base.rb' - - 'lib/gitlab/web_ide/config.rb' - 'lib/gitlab/work_items/work_item_hierarchy.rb' - 'lib/kramdown/parser/atlassian_document_format.rb' - 'lib/tasks/gems.rake' diff --git a/.rubocop_todo/naming/heredoc_delimiter_naming.yml b/.rubocop_todo/naming/heredoc_delimiter_naming.yml index 97355ed07f9..7a782d08a0b 100644 --- a/.rubocop_todo/naming/heredoc_delimiter_naming.yml +++ b/.rubocop_todo/naming/heredoc_delimiter_naming.yml @@ -78,7 +78,6 @@ Naming/HeredocDelimiterNaming: - 'spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb' - 'spec/lib/gitlab/patch/database_config_spec.rb' - 'spec/lib/gitlab/quick_actions/substitution_definition_spec.rb' - - 'spec/lib/gitlab/web_ide/config_spec.rb' - 'spec/lib/gitlab/webpack/file_loader_spec.rb' - 'spec/lib/gitlab/webpack/graphql_known_operations_spec.rb' - 'spec/lib/gitlab/webpack/manifest_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 31f68c13afb..9829c076bef 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -1896,7 +1896,6 @@ RSpec/ContextWording: - 'spec/lib/gitlab/view/presenter/base_spec.rb' - 'spec/lib/gitlab/visibility_level_checker_spec.rb' - 'spec/lib/gitlab/visibility_level_spec.rb' - - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' - 'spec/lib/gitlab/wiki_pages/front_matter_parser_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/gitlab/x509/certificate_spec.rb' diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index 8dd12d76f4b..648ef801f97 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -531,7 +531,6 @@ RSpec/FeatureCategory: - 'ee/spec/lib/ee/gitlab/usage_data_non_sql_metrics_spec.rb' - 'ee/spec/lib/ee/gitlab/verify/lfs_objects_spec.rb' - 'ee/spec/lib/ee/gitlab/verify/uploads_spec.rb' - - 'ee/spec/lib/ee/gitlab/web_ide/config/entry/global_spec.rb' - 'ee/spec/lib/ee/service_ping/service_ping_settings_spec.rb' - 'ee/spec/lib/ee/sidebars/groups/menus/issues_menu_spec.rb' - 'ee/spec/lib/ee/sidebars/projects/menus/ci_cd_menu_spec.rb' @@ -734,10 +733,6 @@ RSpec/FeatureCategory: - 'ee/spec/lib/gitlab/usage_data_counters/streaming_audit_event_type_counter_spec.rb' - 'ee/spec/lib/gitlab/user_access_spec.rb' - 'ee/spec/lib/gitlab/visibility_level_spec.rb' - - 'ee/spec/lib/gitlab/web_ide/config/entry/schema/match_spec.rb' - - 'ee/spec/lib/gitlab/web_ide/config/entry/schema/uri_spec.rb' - - 'ee/spec/lib/gitlab/web_ide/config/entry/schema_spec.rb' - - 'ee/spec/lib/gitlab/web_ide/config/entry/schemas_spec.rb' - 'ee/spec/lib/gitlab_subscriptions/upcoming_reconciliation_entity_spec.rb' - 'ee/spec/lib/incident_management/oncall_shift_generator_spec.rb' - 'ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' @@ -3348,9 +3343,6 @@ RSpec/FeatureCategory: - 'spec/lib/gitlab/visibility_level_spec.rb' - 'spec/lib/gitlab/web_hooks/rate_limiter_spec.rb' - 'spec/lib/gitlab/web_hooks/recursion_detection_spec.rb' - - 'spec/lib/gitlab/web_ide/config/entry/global_spec.rb' - - 'spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' - - 'spec/lib/gitlab/web_ide/config_spec.rb' - 'spec/lib/gitlab/webpack/file_loader_spec.rb' - 'spec/lib/gitlab/webpack/graphql_known_operations_spec.rb' - 'spec/lib/gitlab/webpack/manifest_spec.rb' diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index b13c833208d..06a36348baa 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -321,6 +321,8 @@ class GraphqlController < ApplicationController end def execute_introspection_query + context[:introspection] = true + if introspection_query_can_use_cache? # Context for caching: https://gitlab.com/gitlab-org/gitlab/-/issues/409448 Rails.cache.fetch( diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 474593cdace..1861f0692d8 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -27,11 +27,11 @@ class IdeController < ApplicationController end def oauth_redirect - return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + return render_404 unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) # TODO - It's **possible** we end up here and no oauth application has been set up. # We need to have better handling of these edge cases. Here's a follow-up issue: # https://gitlab.com/gitlab-org/gitlab/-/issues/433322 - return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application + return render_404 unless ::WebIde::DefaultOauthApplication.oauth_application render layout: 'fullscreen', locals: { minimal: true } end @@ -43,9 +43,9 @@ class IdeController < ApplicationController end def ensure_web_ide_oauth_application! - return unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + return unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) - ::Gitlab::WebIde::DefaultOauthApplication.ensure_oauth_application! + ::WebIde::DefaultOauthApplication.ensure_oauth_application! end def fork_info(project, branch) diff --git a/app/finders/organizations/groups_finder.rb b/app/finders/organizations/groups_finder.rb index a95159aaed7..a86f8d77ec3 100644 --- a/app/finders/organizations/groups_finder.rb +++ b/app/finders/organizations/groups_finder.rb @@ -4,6 +4,7 @@ module Organizations class GroupsFinder < GroupsFinder def execute groups = find_union(filtered_groups, Group) + groups = groups.without_deleted if Feature.enabled?(:filter_deleted_groups, current_user) unless default_organization? cte = Gitlab::SQL::CTE.new(:filtered_groups_cte, groups, materialized: false) diff --git a/app/graphql/cached_introspection_query.rb b/app/graphql/cached_introspection_query.rb index f2b98426714..0dda56fae43 100644 --- a/app/graphql/cached_introspection_query.rb +++ b/app/graphql/cached_introspection_query.rb @@ -94,6 +94,14 @@ module CachedIntrospectionQuery ofType { kind name + ofType { + kind + name + ofType { + kind + name + } + } } } } diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 1829de05b72..2b8d670e645 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true class GitlabSchema < GraphQL::Schema - # Currently an IntrospectionQuery has a complexity of 179. - # These values will evolve over time. DEFAULT_MAX_COMPLEXITY = 200 AUTHENTICATED_MAX_COMPLEXITY = 250 ADMIN_MAX_COMPLEXITY = 300 + # Current GraphiQL introspection query has complexity of 217. + # As we cache this specific query we allow it to have a higher complexity. + INTROSPECTION_MAX_COMPLEXITY = 217 DEFAULT_MAX_DEPTH = 15 AUTHENTICATED_MAX_DEPTH = 20 @@ -156,11 +157,14 @@ class GitlabSchema < GraphQL::Schema def max_query_complexity(ctx) current_user = ctx&.fetch(:current_user, nil) + introspection = ctx&.fetch(:introspection, false) if current_user&.admin ADMIN_MAX_COMPLEXITY elsif current_user AUTHENTICATED_MAX_COMPLEXITY + elsif introspection + INTROSPECTION_MAX_COMPLEXITY else DEFAULT_MAX_COMPLEXITY end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index d476a3c08ca..e36dc3c75c6 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -22,15 +22,15 @@ module IdeHelper end def show_web_ide_oauth_callback_mismatch_callout? - return false unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + return false unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) - callback_urls = ::Gitlab::WebIde::DefaultOauthApplication.oauth_application_callback_urls + callback_urls = ::WebIde::DefaultOauthApplication.oauth_application_callback_urls callback_url_domains = callback_urls.map { |url| URI.parse(url).origin } callback_url_domains.any? && callback_url_domains.exclude?(request.base_url) end def web_ide_oauth_application_id - ::Gitlab::WebIde::DefaultOauthApplication.oauth_application_id + ::WebIde::DefaultOauthApplication.oauth_application_id end def use_new_web_ide? @@ -66,11 +66,11 @@ module IdeHelper end def new_ide_oauth_data - return {} unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user) - return {} unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application + return {} unless ::WebIde::DefaultOauthApplication.feature_enabled?(current_user) + return {} unless ::WebIde::DefaultOauthApplication.oauth_application - client_id = ::Gitlab::WebIde::DefaultOauthApplication.oauth_application.uid - callback_urls = ::Gitlab::WebIde::DefaultOauthApplication.oauth_application_callback_urls + client_id = ::WebIde::DefaultOauthApplication.oauth_application.uid + callback_urls = ::WebIde::DefaultOauthApplication.oauth_application_callback_urls { 'client-id' => client_id, @@ -121,6 +121,6 @@ module IdeHelper end def extensions_gallery_settings - Gitlab::WebIde::ExtensionsMarketplace.webide_extensions_gallery_settings(user: current_user).to_json + WebIde::ExtensionsMarketplace.webide_extensions_gallery_settings(user: current_user).to_json end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 4398dcbe742..ed304748da0 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -132,7 +132,7 @@ module PreferencesHelper [].tap do |views| views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod') } if Gitlab::CurrentSettings.gitpod_enabled views << { name: 'sourcegraph', message: sourcegraph_url_message, message_url: Gitlab::CurrentSettings.sourcegraph_url, help_link: help_page_path('user/profile/preferences', anchor: 'sourcegraph') } if Gitlab::Sourcegraph.feature_available? && Gitlab::CurrentSettings.sourcegraph_enabled - views << extensions_marketplace_view if Gitlab::WebIde::ExtensionsMarketplace.feature_enabled?(user: current_user) + views << extensions_marketplace_view if WebIde::ExtensionsMarketplace.feature_enabled?(user: current_user) end end @@ -140,14 +140,14 @@ module PreferencesHelper def extensions_marketplace_view # We handle the linkStart / linkEnd inside of a Vue sprintf - extensions_marketplace_home = "%{linkStart}#{::Gitlab::WebIde::ExtensionsMarketplace.marketplace_home_url}%{linkEnd}" + extensions_marketplace_home = "%{linkStart}#{::WebIde::ExtensionsMarketplace.marketplace_home_url}%{linkEnd}" message = format(s_('PreferencesIntegrations|Uses %{extensions_marketplace_home} as the extension marketplace for the Web IDE.'), extensions_marketplace_home: extensions_marketplace_home) { name: 'extensions_marketplace', message: message, - message_url: Gitlab::WebIde::ExtensionsMarketplace.marketplace_home_url, - help_link: Gitlab::WebIde::ExtensionsMarketplace.help_preferences_url + message_url: WebIde::ExtensionsMarketplace.marketplace_home_url, + help_link: WebIde::ExtensionsMarketplace.help_preferences_url } end diff --git a/app/models/audit_events/group_audit_event.rb b/app/models/audit_events/group_audit_event.rb index 5de11c729ef..1170c9bbdcf 100644 --- a/app/models/audit_events/group_audit_event.rb +++ b/app/models/audit_events/group_audit_event.rb @@ -3,9 +3,40 @@ module AuditEvents class GroupAuditEvent < ApplicationRecord self.table_name = "group_audit_events" - include PartitionedTable - self.primary_key = :id - partitioned_by :created_at, strategy: :monthly + include AuditEvents::CommonModel + include ::Gitlab::Utils::StrongMemoize + + validates :group_id, presence: true + + scope :by_group, ->(group_id) { where(group_id: group_id) } + + attr_accessor :root_group_entity_id + attr_writer :group + + def group + lazy_group + end + strong_memoize_attr :group + + def root_group_entity + return ::Group.find_by(id: root_group_entity_id) if root_group_entity_id.present? + return if group.nil? + + root_group_entity = group.root_ancestor + self.root_group_entity_id = root_group_entity.id + root_group_entity + end + strong_memoize_attr :root_group_entity + + private + + def lazy_group + BatchLoader.for(group_id) + .batch(default_value: ::Gitlab::Audit::NullEntity.new + ) do |ids, loader| + ::Group.where(id: ids).find_each { |record| loader.call(record.id, record) } + end + end end end diff --git a/app/models/audit_events/instance_audit_event.rb b/app/models/audit_events/instance_audit_event.rb index 59555a349c7..466b28413cd 100644 --- a/app/models/audit_events/instance_audit_event.rb +++ b/app/models/audit_events/instance_audit_event.rb @@ -3,9 +3,7 @@ module AuditEvents class InstanceAuditEvent < ApplicationRecord self.table_name = "instance_audit_events" - include PartitionedTable - self.primary_key = :id - partitioned_by :created_at, strategy: :monthly + include AuditEvents::CommonModel end end diff --git a/app/models/audit_events/project_audit_event.rb b/app/models/audit_events/project_audit_event.rb index 0c1c2e42944..d8308947335 100644 --- a/app/models/audit_events/project_audit_event.rb +++ b/app/models/audit_events/project_audit_event.rb @@ -3,9 +3,40 @@ module AuditEvents class ProjectAuditEvent < ApplicationRecord self.table_name = "project_audit_events" - include PartitionedTable - self.primary_key = :id - partitioned_by :created_at, strategy: :monthly + include AuditEvents::CommonModel + include ::Gitlab::Utils::StrongMemoize + + validates :project_id, presence: true + + scope :by_project, ->(project_id) { where(project_id: project_id) } + + attr_accessor :root_group_entity_id + attr_writer :project + + def project + lazy_project + end + strong_memoize_attr :project + + def root_group_entity + return ::Group.find_by(id: root_group_entity_id) if root_group_entity_id.present? + return if project.nil? + + root_group_entity = project.group&.root_ancestor + self.root_group_entity_id = root_group_entity&.id + root_group_entity + end + strong_memoize_attr :root_group_entity + + private + + def lazy_project + BatchLoader.for(project_id) + .batch(default_value: ::Gitlab::Audit::NullEntity.new + ) do |ids, loader| + ::Project.where(id: ids).find_each { |record| loader.call(record.id, record) } + end + end end end diff --git a/app/models/audit_events/user_audit_event.rb b/app/models/audit_events/user_audit_event.rb index 693afba1bb5..4cb6291d87a 100644 --- a/app/models/audit_events/user_audit_event.rb +++ b/app/models/audit_events/user_audit_event.rb @@ -3,9 +3,12 @@ module AuditEvents class UserAuditEvent < ApplicationRecord self.table_name = "user_audit_events" - include PartitionedTable - self.primary_key = :id - partitioned_by :created_at, strategy: :monthly + include AuditEvents::CommonModel + + validates :user_id, presence: true + + scope :by_user, ->(user_id) { where(user_id: user_id) } + scope :by_username, ->(username) { where(user_id: find_user_id(username)) } end end diff --git a/app/models/concerns/audit_events/common_model.rb b/app/models/concerns/audit_events/common_model.rb new file mode 100644 index 00000000000..9f1c0bdc8fc --- /dev/null +++ b/app/models/concerns/audit_events/common_model.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module AuditEvents + module CommonModel + extend ActiveSupport::Concern + + PARALLEL_PERSISTENCE_COLUMNS = [ + :author_name, + :entity_path, + :target_details, + :target_type, + :target_id + ].freeze + + TRUNCATED_FIELDS = { + entity_path: 5_500, + target_details: 5_500 + }.freeze + + included do + include AfterCommitQueue + include CreatedAtFilterable + include BulkInsertSafe + include EachBatch + include PartitionedTable + + self.primary_key = :id + + partitioned_by :created_at, strategy: :monthly + + serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize -- We need this to serialize details stored in audit event. + + belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events + + validates :author_id, presence: true + + validates :ip_address, ip_address: true + + scope :by_author_id, ->(author_id) { where(author_id: author_id) } + scope :by_author_username, ->(username) { where(author_id: find_user_id(username)) } + + after_initialize :initialize_details + + before_validation :sanitize_message + before_validation :truncate_fields + + after_validation :parallel_persist + end + + class_methods do + def supported_keyset_orderings + { id: [:desc] } + end + + def order_by(method) + case method.to_s + when 'created_asc' + order(id: :asc) + else + order(id: :desc) + end + end + + def find_user_id(username) + User.find_by_username(username)&.id + end + end + + def initialize_details + return unless has_attribute?(:details) + + self.details = {} if details&.nil? + end + + def author_name + author&.name + end + + def formatted_details + details + .merge(details.slice(:from, :to).transform_values(&:to_s)) + .merge(author_email: author.try(:email)) + end + + def author + lazy_author&.itself.presence || default_author_value + end + + def lazy_author + BatchLoader.for(author_id).batch do |author_ids, loader| + User.select(:id, :name, :username, :email).where(id: author_ids).find_each do |user| + loader.call(user.id, user) + end + end + end + + def as_json(options = {}) + super(options).tap do |json| + json['ip_address'] = ip_address.to_s + end + end + + def target_type + super || details[:target_type] + end + + def target_id + details[:target_id] + end + + def target_details + super || details[:target_details] + end + + def entity_path + super || details[:entity_path] + end + + def ip_address + super&.to_s || details[:ip_address] + end + + private + + def sanitize_message + message = details[:custom_message] + + return unless message + + self.details = details.merge(custom_message: Sanitize.clean(message)) + end + + def default_author_value + ::Gitlab::Audit::NullAuthor.for(author_id, self) + end + + def parallel_persist + PARALLEL_PERSISTENCE_COLUMNS.each do |name| + original = self[name] || details[name] + next unless original + + self[name] = details[name] = original + end + end + + def truncate_fields + TRUNCATED_FIELDS.each do |name, limit| + original = self[name] || details[name] + next unless original + + self[name] = details[name] = String(original).truncate(limit) + end + end + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 9b03771857e..b852029fe09 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -178,7 +178,8 @@ class Namespace < ApplicationRecord delegate :math_rendering_limits_enabled?, :lock_math_rendering_limits_enabled?, to: :namespace_settings - delegate :add_creator, to: :namespace_details + delegate :add_creator, :pending_delete, :pending_delete=, + to: :namespace_details before_create :sync_share_with_group_lock_with_parent before_update :sync_share_with_group_lock_with_parent, if: :parent_changed? @@ -196,6 +197,7 @@ class Namespace < ApplicationRecord saved_change_to_name?) || saved_change_to_path? || saved_change_to_parent_id? } + scope :without_deleted, -> { joins(:namespace_details).where(namespace_details: { pending_delete: false }) } scope :user_namespaces, -> { where(type: Namespaces::UserNamespace.sti_name) } scope :group_namespaces, -> { where(type: Group.sti_name) } scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].not_eq(Namespaces::ProjectNamespace.sti_name)) } diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index aeab2667737..d961effb669 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -5,6 +5,8 @@ module Groups DestroyError = Class.new(StandardError) def async_execute + mark_pending_delete + job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) Gitlab::AppLogger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end @@ -14,6 +16,8 @@ module Groups # TODO - add a policy check here https://gitlab.com/gitlab-org/gitlab/-/issues/353082 raise DestroyError, "You can't delete this group because you're blocked." if current_user.blocked? + mark_pending_delete + group.projects.includes(:project_feature).each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. success = ::Projects::DestroyService.new(project, current_user).execute @@ -46,11 +50,22 @@ module Groups publish_event group + rescue Exception # rubocop:disable Lint/RescueException -- Namespace.transaction can raise Exception + unmark_pending_delete + raise end # rubocop: enable CodeReuse/ActiveRecord private + def mark_pending_delete + group.update_attribute(:pending_delete, true) + end + + def unmark_pending_delete + group.update_attribute(:pending_delete, false) + end + def any_groups_shared_with_this_group? group.shared_group_links.any? end diff --git a/app/services/ide/base_config_service.rb b/app/services/ide/base_config_service.rb index 0501fab53af..1094abd2622 100644 --- a/app/services/ide/base_config_service.rb +++ b/app/services/ide/base_config_service.rb @@ -41,12 +41,12 @@ module Ide end def load_config! - @config = Gitlab::WebIde::Config.new(config_content) + @config = WebIde::Config.new(config_content) unless @config.valid? raise ValidationError, @config.errors.first end - rescue Gitlab::WebIde::Config::ConfigError => e + rescue WebIde::Config::ConfigError => e raise ValidationError, e.message end diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index d4c651e4ba0..970af9e321a 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -8,7 +8,7 @@ - fluid_help_text = s_('Preferences|Content will span %{percentage} of the page width.').html_safe % { percentage: '100%' } - @color_modes = Gitlab::ColorModes::available_modes.to_json - @themes = Gitlab::Themes::available_themes.to_json -- extensions_marketplace_url = ::Gitlab::WebIde::ExtensionsMarketplace.marketplace_home_url +- extensions_marketplace_url = ::WebIde::ExtensionsMarketplace.marketplace_home_url - data_attributes = { color_modes: @color_modes, themes: @themes, integration_views: integration_views.to_json, user_fields: user_fields, body_classes: Gitlab::Themes.body_classes, profile_preferences_path: profile_preferences_path, extensions_marketplace_url: extensions_marketplace_url } - @force_desktop_expanded_sidebar = true diff --git a/config/bounded_contexts.yml b/config/bounded_contexts.yml index 198187d3961..3a1d223210f 100644 --- a/config/bounded_contexts.yml +++ b/config/bounded_contexts.yml @@ -318,7 +318,7 @@ domains: feature_categories: - webhooks - WebIDE: + WebIde: description: feature_categories: - web_ide diff --git a/config/feature_flags/gitlab_com_derisk/filter_deleted_groups.yml b/config/feature_flags/gitlab_com_derisk/filter_deleted_groups.yml new file mode 100644 index 00000000000..526066186cd --- /dev/null +++ b/config/feature_flags/gitlab_com_derisk/filter_deleted_groups.yml @@ -0,0 +1,9 @@ +--- +name: filter_deleted_groups +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/455871 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158309 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470628 +milestone: '17.2' +group: group::tenant scale +type: gitlab_com_derisk +default_enabled: false diff --git a/db/migrate/20240701072209_add_pending_delete_to_namespace_details.rb b/db/migrate/20240701072209_add_pending_delete_to_namespace_details.rb new file mode 100644 index 00000000000..3fc2fec8ebe --- /dev/null +++ b/db/migrate/20240701072209_add_pending_delete_to_namespace_details.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPendingDeleteToNamespaceDetails < Gitlab::Database::Migration[2.2] + milestone '17.2' + + def change + add_column :namespace_details, :pending_delete, :boolean, default: false, null: false + end +end diff --git a/db/post_migrate/20240702124708_fk_to_ci_pipelines_from_ci_pipeline_artifacts_on_partition_id_and_pipeline_id.rb b/db/post_migrate/20240702124708_fk_to_ci_pipelines_from_ci_pipeline_artifacts_on_partition_id_and_pipeline_id.rb new file mode 100644 index 00000000000..e21b1ffc52e --- /dev/null +++ b/db/post_migrate/20240702124708_fk_to_ci_pipelines_from_ci_pipeline_artifacts_on_partition_id_and_pipeline_id.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class FkToCiPipelinesFromCiPipelineArtifactsOnPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_pipeline_artifacts + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_a9e811a466_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: false, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20240702124709_validate_async_fk_on_ci_pipeline_artifacts_partition_id_and_pipeline_id.rb b/db/post_migrate/20240702124709_validate_async_fk_on_ci_pipeline_artifacts_partition_id_and_pipeline_id.rb new file mode 100644 index 00000000000..37bd1d7df29 --- /dev/null +++ b/db/post_migrate/20240702124709_validate_async_fk_on_ci_pipeline_artifacts_partition_id_and_pipeline_id.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ValidateAsyncFkOnCiPipelineArtifactsPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + + TABLE_NAME = :ci_pipeline_artifacts + FK_NAME = :fk_rails_a9e811a466_p + COLUMNS = [:partition_id, :pipeline_id] + + def up + prepare_async_foreign_key_validation(TABLE_NAME, COLUMNS, name: FK_NAME) + end + + def down + unprepare_async_foreign_key_validation(TABLE_NAME, COLUMNS, name: FK_NAME) + end +end diff --git a/db/post_migrate/20240702143001_fk_ci_pipelines_ci_daily_build_group_report_results_on_par_id_last_pipeline_id.rb b/db/post_migrate/20240702143001_fk_ci_pipelines_ci_daily_build_group_report_results_on_par_id_last_pipeline_id.rb new file mode 100644 index 00000000000..01a472dbb00 --- /dev/null +++ b/db/post_migrate/20240702143001_fk_ci_pipelines_ci_daily_build_group_report_results_on_par_id_last_pipeline_id.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class FkCiPipelinesCiDailyBuildGroupReportResultsOnParIdLastPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_daily_build_group_report_results + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :last_pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_ee072d13b3_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: false, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20240702143002_validate_async_fk_ci_daily_build_group_report_results_par_id_last_pipeline_id.rb b/db/post_migrate/20240702143002_validate_async_fk_ci_daily_build_group_report_results_par_id_last_pipeline_id.rb new file mode 100644 index 00000000000..4004f7368c4 --- /dev/null +++ b/db/post_migrate/20240702143002_validate_async_fk_ci_daily_build_group_report_results_par_id_last_pipeline_id.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class ValidateAsyncFkCiDailyBuildGroupReportResultsParIdLastPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + + TABLE_NAME = :ci_daily_build_group_report_results + FK_NAME = :fk_rails_ee072d13b3_p + COLUMNS = [:partition_id, :last_pipeline_id] + + def up + prepare_async_foreign_key_validation(TABLE_NAME, COLUMNS, name: FK_NAME) + end + + def down + unprepare_async_foreign_key_validation(TABLE_NAME, COLUMNS, name: FK_NAME) + end +end diff --git a/db/post_migrate/20240703113618_fk_to_ci_pipelines_from_ci_sources_projects_on_partition_id_and_pipeline_id.rb b/db/post_migrate/20240703113618_fk_to_ci_pipelines_from_ci_sources_projects_on_partition_id_and_pipeline_id.rb new file mode 100644 index 00000000000..f619bec7cab --- /dev/null +++ b/db/post_migrate/20240703113618_fk_to_ci_pipelines_from_ci_sources_projects_on_partition_id_and_pipeline_id.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class FkToCiPipelinesFromCiSourcesProjectsOnPartitionIdAndPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_sources_projects + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_10a1eb379a_p + PARTITION_COLUMN = :partition_id + + def up + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: [PARTITION_COLUMN, COLUMN], + target_column: [PARTITION_COLUMN, TARGET_COLUMN], + validate: true, + reverse_lock_order: true, + on_update: :cascade, + on_delete: :cascade, + name: FK_NAME + ) + end + + def down + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end +end diff --git a/db/post_migrate/20240703113745_remove_fk_to_ci_pipelines_ci_sources_projects_on_pipeline_id.rb b/db/post_migrate/20240703113745_remove_fk_to_ci_pipelines_ci_sources_projects_on_pipeline_id.rb new file mode 100644 index 00000000000..d91adc70042 --- /dev/null +++ b/db/post_migrate/20240703113745_remove_fk_to_ci_pipelines_ci_sources_projects_on_pipeline_id.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class RemoveFkToCiPipelinesCiSourcesProjectsOnPipelineId < Gitlab::Database::Migration[2.2] + milestone '17.2' + disable_ddl_transaction! + + SOURCE_TABLE_NAME = :ci_sources_projects + TARGET_TABLE_NAME = :ci_pipelines + COLUMN = :pipeline_id + TARGET_COLUMN = :id + FK_NAME = :fk_rails_10a1eb379a + + def up + with_lock_retries do + remove_foreign_key_if_exists( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + name: FK_NAME, + reverse_lock_order: true + ) + end + end + + def down + add_concurrent_foreign_key( + SOURCE_TABLE_NAME, + TARGET_TABLE_NAME, + column: COLUMN, + target_column: TARGET_COLUMN, + validate: true, + reverse_lock_order: true, + on_delete: :cascade, + name: FK_NAME + ) + end +end diff --git a/db/schema_migrations/20240701072209 b/db/schema_migrations/20240701072209 new file mode 100644 index 00000000000..07b80f7b71f --- /dev/null +++ b/db/schema_migrations/20240701072209 @@ -0,0 +1 @@ +9170262cb868bc440128752eedfd21e042f4ea69ec61c987a8af89edf9616f93 \ No newline at end of file diff --git a/db/schema_migrations/20240702124708 b/db/schema_migrations/20240702124708 new file mode 100644 index 00000000000..13a8f588638 --- /dev/null +++ b/db/schema_migrations/20240702124708 @@ -0,0 +1 @@ +fa9bc8a810958a939d97f9c1a9d8f8c57c136cd2de2e5e9b9d9fe96de15380d1 \ No newline at end of file diff --git a/db/schema_migrations/20240702124709 b/db/schema_migrations/20240702124709 new file mode 100644 index 00000000000..289f4000f9e --- /dev/null +++ b/db/schema_migrations/20240702124709 @@ -0,0 +1 @@ +2192a39694251af43e0f4312ae4f8ea97e474edca74fe2ae340e3cbb153cced8 \ No newline at end of file diff --git a/db/schema_migrations/20240702143001 b/db/schema_migrations/20240702143001 new file mode 100644 index 00000000000..dfb3754671d --- /dev/null +++ b/db/schema_migrations/20240702143001 @@ -0,0 +1 @@ +a9ba59901844135e10bf0666a106982c2dcd73e01e1dec6869c8783157f3ea18 \ No newline at end of file diff --git a/db/schema_migrations/20240702143002 b/db/schema_migrations/20240702143002 new file mode 100644 index 00000000000..f64826596c6 --- /dev/null +++ b/db/schema_migrations/20240702143002 @@ -0,0 +1 @@ +4ec6ade0a0b8008ed978cba9a530ae6e5b1efbde0281749fd82fc53d12d58ae3 \ No newline at end of file diff --git a/db/schema_migrations/20240703113618 b/db/schema_migrations/20240703113618 new file mode 100644 index 00000000000..417a79f1e8a --- /dev/null +++ b/db/schema_migrations/20240703113618 @@ -0,0 +1 @@ +ab12a8cb06d71254d8e56022957eebdf3bca931784ca1ccf8efefd6ade9d5aba \ No newline at end of file diff --git a/db/schema_migrations/20240703113745 b/db/schema_migrations/20240703113745 new file mode 100644 index 00000000000..e94fc2cda69 --- /dev/null +++ b/db/schema_migrations/20240703113745 @@ -0,0 +1 @@ +6bb086e637c6559dc6f3a92f9415da3de4ad151fc198343c99348c205e9dd5e2 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index b73160474c3..b4e76906858 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13212,7 +13212,8 @@ CREATE TABLE namespace_details ( cached_markdown_version integer, description text, description_html text, - creator_id bigint + creator_id bigint, + pending_delete boolean DEFAULT false NOT NULL ); CREATE TABLE namespace_ldap_settings ( @@ -33392,7 +33393,7 @@ ALTER TABLE ONLY audit_events_streaming_headers ADD CONSTRAINT fk_rails_109fcf96e2 FOREIGN KEY (external_audit_event_destination_id) REFERENCES audit_events_external_audit_event_destinations(id) ON DELETE CASCADE; ALTER TABLE ONLY ci_sources_projects - ADD CONSTRAINT fk_rails_10a1eb379a FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; + ADD CONSTRAINT fk_rails_10a1eb379a_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE; ALTER TABLE ONLY zoom_meetings ADD CONSTRAINT fk_rails_1190f0e0fa FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -34528,6 +34529,9 @@ ALTER TABLE ONLY saved_replies ALTER TABLE ONLY ci_pipeline_artifacts ADD CONSTRAINT fk_rails_a9e811a466 FOREIGN KEY (pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; +ALTER TABLE ONLY ci_pipeline_artifacts + ADD CONSTRAINT fk_rails_a9e811a466_p FOREIGN KEY (partition_id, pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY merge_request_user_mentions ADD CONSTRAINT fk_rails_aa1b2961b1 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; @@ -35041,6 +35045,9 @@ ALTER TABLE ONLY packages_debian_group_distributions ALTER TABLE ONLY ci_daily_build_group_report_results ADD CONSTRAINT fk_rails_ee072d13b3 FOREIGN KEY (last_pipeline_id) REFERENCES ci_pipelines(id) ON DELETE CASCADE; +ALTER TABLE ONLY ci_daily_build_group_report_results + ADD CONSTRAINT fk_rails_ee072d13b3_p FOREIGN KEY (partition_id, last_pipeline_id) REFERENCES ci_pipelines(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY import_source_users ADD CONSTRAINT fk_rails_ee30e569be FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 7be80f78b94..720ef8f44fd 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -218,8 +218,7 @@ Prerequisites: - You have configured DNS setup [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns). -1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable - the feature flag: +1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: ```ruby # External_url here is only for reference @@ -309,8 +308,7 @@ In this configuration, NGINX proxies all requests to the daemon. The GitLab Page daemon doesn't listen to the outside world: 1. Add your TLS certificate and key as mentioned in the prerequisites into `/etc/gitlab/ssl`. -1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable - the feature flag: +1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable the feature: ```ruby # The external_url field is here only for reference. diff --git a/doc/api/settings.md b/doc/api/settings.md index 51292da9403..db5c07e3bad 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -694,6 +694,7 @@ listed in the descriptions of the relevant settings. | `duo_features_enabled` | boolean | no | Indicates whether GitLab Duo features are enabled for this instance. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144931) in GitLab 16.10. Self-managed, Premium and Ultimate only. | | `lock_duo_features_enabled` | boolean | no | Indicates whether the GitLab Duo features enabled setting is enforced for all subgroups. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/144931) in GitLab 16.10. Self-managed, Premium and Ultimate only. | | `nuget_skip_metadata_url_validation` | boolean | no | Indicates whether to skip metadata URL validation for the NuGet package. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145887) in GitLab 17.0. | +| `require_admin_two_factor_authentication` | boolean | no | Allow administrators to require 2FA for all administrators on the instance. | ### Configure inactive project deletion diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md index 66fe6a9d47a..eae4bb8e7d5 100644 --- a/doc/user/workspace/index.md +++ b/doc/user/workspace/index.md @@ -177,12 +177,22 @@ must meet the following system requirements: These requirements have been tested on Debian 10.13 and Ubuntu 20.04. For more information, see the [VS Code documentation](https://code.visualstudio.com/docs/remote/linux). +## Workspace add-ons + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385157) in GitLab 17.2. + +The GitLab Workflow extension for VS Code is configured by default in workspaces. +With this extension, you can view issues, create and review merge requests, and manage CI/CD pipelines. +The extension also powers AI features like GitLab Duo Code Suggestions and GitLab Duo Chat. +For more information, see [GitLab Workflow extension for VS Code](https://gitlab.com/gitlab-org/gitlab-vscode-extension). + ## Personal access token > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129715) in GitLab 16.4. +> - `api` permission [added](https://gitlab.com/gitlab-org/gitlab/-/issues/385157) in GitLab 17.2. -When you [create a workspace](configuration.md#create-a-workspace), you get a personal access token with `write_repository` permission. -This token is used to initially clone the project while starting the workspace. +When you [create a workspace](configuration.md#create-a-workspace), you get a personal access token with `write_repository` and `api` permissions. +This token is used to initially clone the project while starting the workspace and to configure the GitLab Workflow extension for VS Code. Any Git operation you perform in the workspace uses this token for authentication and authorization. When you terminate the workspace, the token is revoked. diff --git a/lib/gitlab/web_ide/config.rb b/lib/gitlab/web_ide/config.rb deleted file mode 100644 index b2ab5c0b6e3..00000000000 --- a/lib/gitlab/web_ide/config.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIde - # - # Base GitLab WebIde Configuration facade - # - class Config - ConfigError = Class.new(StandardError) - - def initialize(config, opts = {}) - @config = build_config(config, opts) - - @global = Entry::Global.new(@config, - with_image_ports: true) - @global.compose! - rescue Gitlab::Config::Loader::FormatError => e - raise Config::ConfigError, e.message - end - - def valid? - @global.valid? - end - - def errors - @global.errors - end - - def to_hash - @config - end - - def terminal_value - @global.terminal_value - end - - def schemas_value - @global.schemas_value - end - - private - - def build_config(config, opts = {}) - Gitlab::Config::Loader::Yaml.new(config).load! - end - end - end -end diff --git a/lib/gitlab/web_ide/config/entry/global.rb b/lib/gitlab/web_ide/config/entry/global.rb deleted file mode 100644 index 2939095fd0f..00000000000 --- a/lib/gitlab/web_ide/config/entry/global.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIde - class Config - module Entry - ## - # This class represents a global entry - root Entry for entire - # GitLab WebIde Configuration file. - # - class Global < ::Gitlab::Config::Entry::Node - include ::Gitlab::Config::Entry::Configurable - include ::Gitlab::Config::Entry::Attributable - - def self.allowed_keys - %i[terminal].freeze - end - - validations do - validates :config, allowed_keys: Global.allowed_keys - end - - attributes allowed_keys - - entry :terminal, Entry::Terminal, - description: 'Configuration of the webide terminal.' - end - end - end - end -end - -::Gitlab::WebIde::Config::Entry::Global.prepend_mod_with('Gitlab::WebIde::Config::Entry::Global') diff --git a/lib/gitlab/web_ide/config/entry/terminal.rb b/lib/gitlab/web_ide/config/entry/terminal.rb deleted file mode 100644 index b2f0e0285eb..00000000000 --- a/lib/gitlab/web_ide/config/entry/terminal.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIde - class Config - module Entry - ## - # Entry that represents a concrete CI/CD job. - # - class Terminal < ::Gitlab::Config::Entry::Node - include ::Gitlab::Config::Entry::Configurable - include ::Gitlab::Config::Entry::Attributable - include Gitlab::Utils::StrongMemoize - - # By default the build will finish in a few seconds, not giving the webide - # enough time to connect to the terminal. This default script provides - # those seconds blocking the build from finishing inmediately. - DEFAULT_SCRIPT = ['sleep 60'].freeze - - ALLOWED_KEYS = %i[image services tags before_script script variables].freeze - - validations do - validates :config, allowed_keys: ALLOWED_KEYS - validates :config, job_port_unique: { data: ->(record) { record.ports } } - - with_options allow_nil: true do - validates :tags, array_of_strings: true - end - end - - entry :before_script, ::Gitlab::Ci::Config::Entry::Commands, - description: 'Global before script overridden in this job.' - - entry :script, ::Gitlab::Ci::Config::Entry::Commands, - description: 'Commands that will be executed in this job.' - - entry :image, ::Gitlab::Ci::Config::Entry::Image, - description: 'Image that will be used to execute this job.' - - entry :services, ::Gitlab::Ci::Config::Entry::Services, - description: 'Services that will be used to execute this job.' - - entry :variables, ::Gitlab::Ci::Config::Entry::Variables, - description: 'Environment variables available for this job.' - - attributes :tags - - def value - to_hash.compact - end - - private - - def to_hash - { - tag_list: tags || [], - job_variables: yaml_variables, - options: { - image: image_value, - services: services_value, - before_script: before_script_value, - script: script_value || DEFAULT_SCRIPT - }.compact - }.compact - end - - def yaml_variables - strong_memoize(:yaml_variables) do - next unless variables_value - - variables_value.map do |key, value| - { key: key.to_s, value: value, public: true } - end - end - end - end - end - end - end -end diff --git a/lib/gitlab/web_ide/default_oauth_application.rb b/lib/gitlab/web_ide/default_oauth_application.rb deleted file mode 100644 index 6b0ce12ca37..00000000000 --- a/lib/gitlab/web_ide/default_oauth_application.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIde - module DefaultOauthApplication - class << self - def feature_enabled?(current_user) - Feature.enabled?(:vscode_web_ide, current_user) && Feature.enabled?(:web_ide_oauth, current_user) - end - - def oauth_application - application_settings.web_ide_oauth_application - end - - def oauth_callback_url - Gitlab::Routing.url_helpers.ide_oauth_redirect_url - end - - def oauth_application_id - oauth_application ? oauth_application.id : nil - end - - def oauth_application_callback_urls - return [] unless oauth_application - - URI.extract(oauth_application.redirect_uri, %w[http https]).uniq - end - - def ensure_oauth_application! - return if oauth_application - - should_expire_cache = false - - application_settings.transaction do - # note: This should run very rarely and should be safe for us to do a lock - # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132496#note_1587293087 - application_settings.lock! - - # note: `lock!`` breaks applicaiton_settings cache and will trigger another query. - # We need to double check here so that requests previously waiting on the lock can - # now just skip. - next if oauth_application - - application = Doorkeeper::Application.new( - name: 'GitLab Web IDE', - redirect_uri: oauth_callback_url, - scopes: ['api'], - trusted: true, - confidential: false) - application.save! - application_settings.update!(web_ide_oauth_application: application) - should_expire_cache = true - end - - # note: This needs to happen outside the transaction, but only if we actually changed something - ::Gitlab::CurrentSettings.expire_current_application_settings if should_expire_cache - end - - private - - def application_settings - ::Gitlab::CurrentSettings.current_application_settings - end - end - end - end -end diff --git a/lib/gitlab/web_ide/extensions_marketplace.rb b/lib/gitlab/web_ide/extensions_marketplace.rb deleted file mode 100644 index 567f7d24743..00000000000 --- a/lib/gitlab/web_ide/extensions_marketplace.rb +++ /dev/null @@ -1,120 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module WebIde - module ExtensionsMarketplace - # NOTE: These `disabled_reason` enumeration values are also referenced/consumed in - # the "gitlab-web-ide" and "gitlab-web-ide-vscode-fork" projects - # (https://gitlab.com/gitlab-org/gitlab-web-ide & https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork), - # so we must ensure that any changes made here are also reflected in those projects. - DISABLED_REASONS = - %i[ - no_user - no_flag - instance_disabled - opt_in_unset - opt_in_disabled - ].to_h { |reason| [reason, reason] }.freeze - - class << self - def feature_enabled?(user:) - # TODO: Add instance-level setting for this https://gitlab.com/gitlab-org/gitlab/-/issues/451871 - - # note: OAuth **must** be enabled for us to use the extension marketplace - ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(user) && - Feature.enabled?(:web_ide_extensions_marketplace, user) - end - - def vscode_settings - # TODO: Add instance-level setting for this https://gitlab.com/gitlab-org/gitlab/-/issues/451871 - # TODO: We need to harmonize this with `lib/remote_development/settings/defaults_initializer.rb` - # https://gitlab.com/gitlab-org/gitlab/-/issues/460515 - { - item_url: 'https://open-vsx.org/vscode/item', - service_url: 'https://open-vsx.org/vscode/gallery', - resource_url_template: - 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}', - control_url: '', - nls_base_url: '', - publisher_url: '' - } - end - - # This value is used when the end-user is accepting the third-party extension marketplace integration. - def marketplace_home_url - "https://open-vsx.org" - end - - def help_url - ::Gitlab::Routing.url_helpers.help_page_url('user/project/web_ide/index', anchor: 'extension-marketplace') - end - - def help_preferences_url - ::Gitlab::Routing.url_helpers.help_page_url('user/profile/preferences', - anchor: 'integrate-with-the-extension-marketplace') - end - - def user_preferences_url - ::Gitlab::Routing.url_helpers.profile_preferences_url(anchor: 'integrations') - end - - # This returns a value to be used in the Web IDE config `extensionsGallerySettings` - # It should match the type expected by the Web IDE: - # - # - https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/51f9e91f890752596e7a3ef51f436fea07885eff/packages/web-ide-types/src/config.ts#L109 - # - # @return [Hash] - def webide_extensions_gallery_settings(user:) - flag_enabled = feature_enabled?(user: user) - metadata = metadata_for_user(user: user, flag_enabled: flag_enabled) - - return { enabled: true, vscode_settings: vscode_settings } if metadata.fetch(:enabled) - - disabled_reason = metadata.fetch(:disabled_reason, nil) - result = { enabled: false, reason: disabled_reason, help_url: help_url } - - if disabled_reason == :opt_in_unset || disabled_reason == :opt_in_disabled - result[:user_preferences_url] = user_preferences_url - end - - result - end - - # @param [User, nil] user - # @param [Boolean, nil] flag_enabled - # @return [Hash] - def metadata_for_user(user:, flag_enabled:) - return metadata_disabled(:no_user) unless user - return metadata_disabled(:no_flag) if flag_enabled.nil? - return metadata_disabled(:instance_disabled) unless flag_enabled - - # noinspection RubyNilAnalysis -- RubyMine doesn't realize user can't be nil because of guard clause above - opt_in_status = user.extensions_marketplace_opt_in_status.to_sym - - case opt_in_status - when :enabled - return metadata_enabled - when :unset - return metadata_disabled(:opt_in_unset) - when :disabled - return metadata_disabled(:opt_in_disabled) - end - - # This is an internal bug due to an enumeration mismatch/inconsistency with the model - raise "Invalid user.extensions_marketplace_opt_in_status: '#{opt_in_status}'. " \ - "Supported statuses are: #{Enums::WebIde::ExtensionsMarketplaceOptInStatus.statuses.keys}." # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is already changed in the next version of gitlab-styles - end - - private - - def metadata_enabled - { enabled: true } - end - - def metadata_disabled(reason) - { enabled: false, disabled_reason: DISABLED_REASONS.fetch(reason) } - end - end - end - end -end diff --git a/lib/remote_development/settings/extensions_gallery_metadata_generator.rb b/lib/remote_development/settings/extensions_gallery_metadata_generator.rb index c7cb4e09370..454ae280528 100644 --- a/lib/remote_development/settings/extensions_gallery_metadata_generator.rb +++ b/lib/remote_development/settings/extensions_gallery_metadata_generator.rb @@ -16,7 +16,7 @@ module RemoteDevelopment extensions_marketplace_feature_flag_enabled } - extensions_gallery_metadata = ::Gitlab::WebIde::ExtensionsMarketplace.metadata_for_user( + extensions_gallery_metadata = ::WebIde::ExtensionsMarketplace.metadata_for_user( user: user, flag_enabled: extensions_marketplace_feature_flag_enabled ) diff --git a/lib/web_ide/config.rb b/lib/web_ide/config.rb new file mode 100644 index 00000000000..9e002b5bfce --- /dev/null +++ b/lib/web_ide/config.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module WebIde + # + # Base GitLab WebIde Configuration facade + # + class Config + ConfigError = Class.new(StandardError) + + def initialize(config, opts = {}) + @config = build_config(config, opts) + + @global = Entry::Global.new(@config, + with_image_ports: true) + @global.compose! + rescue Gitlab::Config::Loader::FormatError => e + raise Config::ConfigError, e.message + end + + def valid? + @global.valid? + end + + def errors + @global.errors + end + + def to_hash + @config + end + + def terminal_value + @global.terminal_value + end + + def schemas_value + @global.schemas_value + end + + private + + def build_config(config, _opts = {}) + Gitlab::Config::Loader::Yaml.new(config).load! + end + end +end diff --git a/lib/web_ide/config/entry/global.rb b/lib/web_ide/config/entry/global.rb new file mode 100644 index 00000000000..0c37200ddb0 --- /dev/null +++ b/lib/web_ide/config/entry/global.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module WebIde + class Config + module Entry + ## + # This class represents a global entry - root Entry for entire + # GitLab WebIde Configuration file. + # + class Global < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable + + def self.allowed_keys + %i[terminal].freeze + end + + validations do + validates :config, allowed_keys: Global.allowed_keys + end + + attributes allowed_keys + + entry :terminal, Entry::Terminal, + description: 'Configuration of the webide terminal.' + end + end + end +end + +::WebIde::Config::Entry::Global.prepend_mod_with('WebIde::Config::Entry::Global') diff --git a/lib/web_ide/config/entry/terminal.rb b/lib/web_ide/config/entry/terminal.rb new file mode 100644 index 00000000000..6a4507ecaf9 --- /dev/null +++ b/lib/web_ide/config/entry/terminal.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module WebIde + class Config + module Entry + ## + # Entry that represents a concrete CI/CD job. + # + class Terminal < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Configurable + include ::Gitlab::Config::Entry::Attributable + include Gitlab::Utils::StrongMemoize + + # By default the build will finish in a few seconds, not giving the webide + # enough time to connect to the terminal. This default script provides + # those seconds blocking the build from finishing inmediately. + DEFAULT_SCRIPT = ['sleep 60'].freeze + + ALLOWED_KEYS = %i[image services tags before_script script variables].freeze + + validations do + validates :config, allowed_keys: ALLOWED_KEYS + validates :config, job_port_unique: { data: ->(record) { record.ports } } + + with_options allow_nil: true do + validates :tags, array_of_strings: true + end + end + + entry :before_script, ::Gitlab::Ci::Config::Entry::Commands, + description: 'Global before script overridden in this job.' + + entry :script, ::Gitlab::Ci::Config::Entry::Commands, + description: 'Commands that will be executed in this job.' + + entry :image, ::Gitlab::Ci::Config::Entry::Image, + description: 'Image that will be used to execute this job.' + + entry :services, ::Gitlab::Ci::Config::Entry::Services, + description: 'Services that will be used to execute this job.' + + entry :variables, ::Gitlab::Ci::Config::Entry::Variables, + description: 'Environment variables available for this job.' + + attributes :tags + + def value + to_hash.compact + end + + private + + def to_hash + { + tag_list: tags || [], + job_variables: yaml_variables, + options: { + image: image_value, + services: services_value, + before_script: before_script_value, + script: script_value || DEFAULT_SCRIPT + }.compact + }.compact + end + + def yaml_variables + strong_memoize(:yaml_variables) do # rubocop:todo Gitlab/StrongMemoizeAttr -- legacy Web IDE is deprecated, not fixing this because. Also not sure if strong_memoize_attr will change behavior + next unless variables_value + + variables_value.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + end + end + end + end +end diff --git a/lib/web_ide/default_oauth_application.rb b/lib/web_ide/default_oauth_application.rb new file mode 100644 index 00000000000..b9108914c9a --- /dev/null +++ b/lib/web_ide/default_oauth_application.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module WebIde + module DefaultOauthApplication + class << self + def feature_enabled?(current_user) + Feature.enabled?(:vscode_web_ide, current_user) && Feature.enabled?(:web_ide_oauth, current_user) + end + + def oauth_application + application_settings.web_ide_oauth_application + end + + def oauth_callback_url + Gitlab::Routing.url_helpers.ide_oauth_redirect_url + end + + def oauth_application_id + oauth_application ? oauth_application.id : nil + end + + def oauth_application_callback_urls + return [] unless oauth_application + + URI.extract(oauth_application.redirect_uri, %w[http https]).uniq + end + + def ensure_oauth_application! + return if oauth_application + + should_expire_cache = false + + application_settings.transaction do + # note: This should run very rarely and should be safe for us to do a lock + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132496#note_1587293087 + application_settings.lock! + + # note: `lock!`` breaks applicaiton_settings cache and will trigger another query. + # We need to double check here so that requests previously waiting on the lock can + # now just skip. + next if oauth_application + + application = Doorkeeper::Application.new( + name: 'GitLab Web IDE', + redirect_uri: oauth_callback_url, + scopes: ['api'], + trusted: true, + confidential: false) + application.save! + application_settings.update!(web_ide_oauth_application: application) + should_expire_cache = true + end + + # note: This needs to happen outside the transaction, but only if we actually changed something + ::Gitlab::CurrentSettings.expire_current_application_settings if should_expire_cache + end + + private + + def application_settings + ::Gitlab::CurrentSettings.current_application_settings + end + end + end +end diff --git a/lib/web_ide/extensions_marketplace.rb b/lib/web_ide/extensions_marketplace.rb new file mode 100644 index 00000000000..43dfabea549 --- /dev/null +++ b/lib/web_ide/extensions_marketplace.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +module WebIde + module ExtensionsMarketplace + # NOTE: These `disabled_reason` enumeration values are also referenced/consumed in + # the "gitlab-web-ide" and "gitlab-web-ide-vscode-fork" projects + # (https://gitlab.com/gitlab-org/gitlab-web-ide & https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork), + # so we must ensure that any changes made here are also reflected in those projects. + DISABLED_REASONS = + %i[ + no_user + no_flag + instance_disabled + opt_in_unset + opt_in_disabled + ].to_h { |reason| [reason, reason] }.freeze + + class << self + def feature_enabled?(user:) + # TODO: Add instance-level setting for this https://gitlab.com/gitlab-org/gitlab/-/issues/451871 + + # note: OAuth **must** be enabled for us to use the extension marketplace + ::WebIde::DefaultOauthApplication.feature_enabled?(user) && + Feature.enabled?(:web_ide_extensions_marketplace, user) + end + + def vscode_settings + # TODO: Add instance-level setting for this https://gitlab.com/gitlab-org/gitlab/-/issues/451871 + # TODO: We need to harmonize this with `lib/remote_development/settings/defaults_initializer.rb` + # https://gitlab.com/gitlab-org/gitlab/-/issues/460515 + { + item_url: 'https://open-vsx.org/vscode/item', + service_url: 'https://open-vsx.org/vscode/gallery', + resource_url_template: + 'https://open-vsx.org/vscode/unpkg/{publisher}/{name}/{version}/{path}', + control_url: '', + nls_base_url: '', + publisher_url: '' + } + end + + # This value is used when the end-user is accepting the third-party extension marketplace integration. + def marketplace_home_url + "https://open-vsx.org" + end + + def help_url + ::Gitlab::Routing.url_helpers.help_page_url('user/project/web_ide/index', anchor: 'extension-marketplace') + end + + def help_preferences_url + ::Gitlab::Routing.url_helpers.help_page_url('user/profile/preferences', + anchor: 'integrate-with-the-extension-marketplace') + end + + def user_preferences_url + ::Gitlab::Routing.url_helpers.profile_preferences_url(anchor: 'integrations') + end + + # This returns a value to be used in the Web IDE config `extensionsGallerySettings` + # It should match the type expected by the Web IDE: + # + # - https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/51f9e91f890752596e7a3ef51f436fea07885eff/packages/web-ide-types/src/config.ts#L109 + # + # @return [Hash] + def webide_extensions_gallery_settings(user:) + flag_enabled = feature_enabled?(user: user) + metadata = metadata_for_user(user: user, flag_enabled: flag_enabled) + + return { enabled: true, vscode_settings: vscode_settings } if metadata.fetch(:enabled) + + disabled_reason = metadata.fetch(:disabled_reason, nil) + result = { enabled: false, reason: disabled_reason, help_url: help_url } + + if disabled_reason == :opt_in_unset || disabled_reason == :opt_in_disabled + result[:user_preferences_url] = user_preferences_url + end + + result + end + + # @param [User, nil] user + # @param [Boolean, nil] flag_enabled + # @return [Hash] + def metadata_for_user(user:, flag_enabled:) + return metadata_disabled(:no_user) unless user + return metadata_disabled(:no_flag) if flag_enabled.nil? + return metadata_disabled(:instance_disabled) unless flag_enabled + + # noinspection RubyNilAnalysis -- RubyMine doesn't realize user can't be nil because of guard clause above + opt_in_status = user.extensions_marketplace_opt_in_status.to_sym + + case opt_in_status + when :enabled + metadata_enabled + when :unset + metadata_disabled(:opt_in_unset) + when :disabled + metadata_disabled(:opt_in_disabled) + else + # This is an internal bug due to an enumeration mismatch/inconsistency with the model + raise "Invalid user.extensions_marketplace_opt_in_status: '#{opt_in_status}'. " \ + "Supported statuses are: #{Enums::WebIde::ExtensionsMarketplaceOptInStatus.statuses.keys}." # rubocop:disable Layout/LineEndStringConcatenationIndentation -- This is already changed in the next version of gitlab-styles + end + end + + private + + def metadata_enabled + { enabled: true } + end + + def metadata_disabled(reason) + { enabled: false, disabled_reason: DISABLED_REASONS.fetch(reason) } + end + end + end +end diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index d253db61ff5..fee23511731 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -18,6 +18,9 @@ RSpec.describe 'Database schema', feature_category: :database do slack_integrations_scopes: [%w[slack_api_scope_id]], notes: %w[namespace_id], # this index is added in an async manner, hence it needs to be ignored in the first phase. users: [%w[accepted_term_id]], + ci_pipeline_artifacts: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient + ci_sources_projects: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient + ci_daily_build_group_report_results: [%w[partition_id last_pipeline_id]], # index on last_pipeline_id is sufficient ci_builds: [%w[partition_id stage_id], %w[partition_id execution_config_id], %w[partition_id upstream_pipeline_id], %w[auto_canceled_by_partition_id auto_canceled_by_id], %w[partition_id commit_id]], # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142804#note_1745483081 ci_pipeline_variables: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient ci_pipelines_config: [%w[partition_id pipeline_id]], # index on pipeline_id is sufficient diff --git a/spec/factories/audit_events/group_audit_events.rb b/spec/factories/audit_events/group_audit_events.rb new file mode 100644 index 00000000000..ba8183390e6 --- /dev/null +++ b/spec/factories/audit_events/group_audit_events.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :audit_events_group_audit_event, class: 'AuditEvents::GroupAuditEvent' do + user + + transient { target_group { association(:group) } } + + group_id { target_group.id } + entity_path { target_group.full_path } + target_details { target_group.name } + ip_address { IPAddr.new '127.0.0.1' } + details do + { + change: 'project_creation_level', + from: nil, + to: 'Developers + Maintainers', + author_name: 'Jane Doe', + target_id: target_group.id, + target_type: 'Group', + target_details: target_group.name, + ip_address: '127.0.0.1', + entity_path: target_group.full_path + } + end + end +end diff --git a/spec/factories/audit_events/instance_audit_events.rb b/spec/factories/audit_events/instance_audit_events.rb new file mode 100644 index 00000000000..5ec20d0b4b9 --- /dev/null +++ b/spec/factories/audit_events/instance_audit_events.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :audit_events_instance_audit_event, class: 'AuditEvents::InstanceAuditEvent' do + user + + entity_path { "gitlab_instance" } + target_details { "Default project visibility" } + ip_address { IPAddr.new '127.0.0.1' } + author_name { 'Jane Doe' } + details do + { + change: "default_project_visibility", + from: 0, + to: 10, + target_details: "Default project visibility", + event_name: "application_setting_updated", + author_name: 'Jane Doe', + target_id: 1, + target_type: "ApplicationSetting", + custom_message: "Changed default_project_visibility from 0 to 10", + ip_address: "127.0.0.1", + entity_path: "gitlab_instance" + } + end + end +end diff --git a/spec/factories/audit_events/project_audit_events.rb b/spec/factories/audit_events/project_audit_events.rb new file mode 100644 index 00000000000..03965e15b25 --- /dev/null +++ b/spec/factories/audit_events/project_audit_events.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :audit_events_project_audit_event, class: 'AuditEvents::ProjectAuditEvent' do + user + + transient { target_project { association(:project) } } + + project_id { target_project.id } + entity_path { target_project.full_path } + target_details { target_project.name } + ip_address { IPAddr.new '127.0.0.1' } + details do + { + change: 'packages_enabled', + from: true, + to: false, + author_name: user.name, + target_id: target_project.id, + target_type: 'Project', + target_details: target_project.name, + ip_address: '127.0.0.1', + entity_path: target_project.full_path + } + end + end +end diff --git a/spec/factories/audit_events/user_audit_events.rb b/spec/factories/audit_events/user_audit_events.rb new file mode 100644 index 00000000000..656c7c0ca02 --- /dev/null +++ b/spec/factories/audit_events/user_audit_events.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :audit_events_user_audit_event, class: 'AuditEvents::UserAuditEvent' do + user + + user_id { user.id } + entity_path { user.full_path } + target_details { user.name } + ip_address { IPAddr.new '127.0.0.1' } + author_name { 'Jane Doe' } + details do + { + change: 'email address', + from: 'admin@gitlab.com', + to: 'maintainer@gitlab.com', + author_name: 'Jane Doe', + target_id: user.id, + target_type: 'User', + target_details: user.name, + ip_address: '127.0.0.1', + entity_path: user.full_path + } + end + end +end diff --git a/spec/finders/organizations/groups_finder_spec.rb b/spec/finders/organizations/groups_finder_spec.rb index 2d2ee009f17..b24c8b9c088 100644 --- a/spec/finders/organizations/groups_finder_spec.rb +++ b/spec/finders/organizations/groups_finder_spec.rb @@ -7,10 +7,15 @@ RSpec.describe Organizations::GroupsFinder, feature_category: :groups_and_projec let_it_be(:organization_user) { create(:organization_user) } let_it_be(:organization) { organization_user.organization } let_it_be(:user) { organization_user.user } - let_it_be(:public_group) { create(:group, name: 'public-group', organization: organization) } - let_it_be(:outside_organization_group) { create(:group) } - let_it_be(:private_group) { create(:group, :private, name: 'private-group', organization: organization) } - let_it_be(:no_access_group_in_org) { create(:group, :private, name: 'no-access', organization: organization) } + let_it_be_with_reload(:public_group) { create(:group, name: 'public-group', organization: organization) } + let_it_be_with_reload(:outside_organization_group) { create(:group) } + let_it_be_with_reload(:private_group) do + create(:group, :private, name: 'private-group', organization: organization) + end + + let_it_be_with_reload(:no_access_group_in_org) do + create(:group, :private, name: 'no-access', organization: organization) + end let(:current_user) { user } let(:params) { { organization: organization } } @@ -59,5 +64,23 @@ RSpec.describe Organizations::GroupsFinder, feature_category: :groups_and_projec result end end + + it 'filters deleted groups' do + public_group.namespace_details.update!(pending_delete: true) + + expect(result).not_to include(public_group) + end + + context 'when filter_deleted_groups feature flag is disabled' do + before do + stub_feature_flags(filter_deleted_groups: false) + end + + it 'includes deleted groups' do + public_group.namespace_details.update!(pending_delete: true) + + expect(result).to include(public_group) + end + end end end diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql index d17da75f352..73ad9764ccd 100644 --- a/spec/fixtures/api/graphql/introspection.graphql +++ b/spec/fixtures/api/graphql/introspection.graphql @@ -89,6 +89,14 @@ fragment TypeRef on __Type { ofType { kind name + ofType { + kind + name + ofType { + kind + name + } + } } } } diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js index 53fbe105ec6..38214531379 100644 --- a/spec/frontend/environment.js +++ b/spec/frontend/environment.js @@ -21,9 +21,12 @@ class CustomEnvironment extends TestEnvironment { const { error: originalErrorFn } = context.console; Object.assign(context.console, { error(...args) { + const firstError = args?.[0]; if ( - args?.[0]?.includes('[Vue warn]: Missing required prop') || - args?.[0]?.includes('[Vue warn]: Invalid prop') + typeof firstError === 'string' && + ['[Vue warn]: Missing required prop', '[Vue warn]: Invalid prop'].some((line) => + firstError.startsWith(line), + ) ) { originalErrorFn.apply(context.console, args); return; diff --git a/spec/helpers/ide_helper_spec.rb b/spec/helpers/ide_helper_spec.rb index 6713e0985eb..59671f02e86 100644 --- a/spec/helpers/ide_helper_spec.rb +++ b/spec/helpers/ide_helper_spec.rb @@ -84,7 +84,7 @@ RSpec.describe IdeHelper, feature_category: :web_ide do end it 'includes extensions gallery settings' do - expect(Gitlab::WebIde::ExtensionsMarketplace).to receive(:webide_extensions_gallery_settings) + expect(WebIde::ExtensionsMarketplace).to receive(:webide_extensions_gallery_settings) .with(user: user).and_return({ enabled: false }) actual = helper.ide_data(project: nil, fork_info: fork_info, params: params) diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index c8f5a0e2215..f101676437e 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -280,7 +280,7 @@ RSpec.describe PreferencesHelper do context 'when WebIdeExtensionsMarketplace is enabled' do before do - allow(Gitlab::WebIde::ExtensionsMarketplace).to receive(:feature_enabled?).with(user: user).and_return(true) + allow(WebIde::ExtensionsMarketplace).to receive(:feature_enabled?).with(user: user).and_return(true) end it 'includes extension marketplace integration' do diff --git a/spec/lib/gitlab/background_migration/backfill_partition_id_ci_daily_build_group_report_result_spec.rb b/spec/lib/gitlab/background_migration/backfill_partition_id_ci_daily_build_group_report_result_spec.rb index ac76630b264..e59513189b9 100644 --- a/spec/lib/gitlab/background_migration/backfill_partition_id_ci_daily_build_group_report_result_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_partition_id_ci_daily_build_group_report_result_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiDailyBuildGroup let(:ci_daily_build_group_report_results_table) { table(:ci_daily_build_group_report_results, database: :ci) } let!(:pipeline_1) { ci_pipelines_table.create!(id: 1, partition_id: 100) } let!(:pipeline_2) { ci_pipelines_table.create!(id: 2, partition_id: 101) } - let!(:pipeline_3) { ci_pipelines_table.create!(id: 3, partition_id: 101) } + let!(:pipeline_3) { ci_pipelines_table.create!(id: 3, partition_id: 100) } let!(:ci_daily_build_group_report_results_100) do ci_daily_build_group_report_results_table.create!( date: Date.yesterday, @@ -43,7 +43,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiDailyBuildGroup data: { 'coverage' => 77.0 }, default_branch: true, last_pipeline_id: pipeline_3.id, - partition_id: pipeline_1.partition_id + partition_id: pipeline_3.partition_id ) end @@ -55,18 +55,29 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiDailyBuildGroup batch_column: :id, sub_batch_size: 1, pause_ms: 0, - connection: Ci::ApplicationRecord.connection + connection: connection } end let!(:migration) { described_class.new(**migration_attrs) } + let(:connection) { Ci::ApplicationRecord.connection } + + around do |example| + connection.transaction do + connection.execute(<<~SQL) + ALTER TABLE ci_pipelines DISABLE TRIGGER ALL; + SQL + + example.run + + connection.execute(<<~SQL) + ALTER TABLE ci_pipelines ENABLE TRIGGER ALL; + SQL + end + end describe '#perform' do context 'when second partition does not exist' do - before do - pipeline_3.update!(partition_id: 100) - end - it 'does not execute the migration' do expect { migration.perform } .not_to change { invalid_ci_daily_build_group_report_results.reload.partition_id } @@ -74,6 +85,10 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiDailyBuildGroup end context 'when second partition exists' do + before do + pipeline_3.update!(partition_id: 101) + end + it 'fixes invalid records in the wrong the partition' do expect { migration.perform } .to not_change { ci_daily_build_group_report_results_100.reload.partition_id } diff --git a/spec/lib/gitlab/background_migration/backfill_partition_id_ci_pipeline_artifact_spec.rb b/spec/lib/gitlab/background_migration/backfill_partition_id_ci_pipeline_artifact_spec.rb index c466fdaa36a..e4968c43972 100644 --- a/spec/lib/gitlab/background_migration/backfill_partition_id_ci_pipeline_artifact_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_partition_id_ci_pipeline_artifact_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiPipelineArtifac let(:ci_pipeline_artifacts_table) { table(:ci_pipeline_artifacts, database: :ci) } let!(:pipeline_100) { ci_pipelines_table.create!(id: 1, partition_id: 100) } let!(:pipeline_101) { ci_pipelines_table.create!(id: 2, partition_id: 101) } - let!(:pipeline_102) { ci_pipelines_table.create!(id: 3, partition_id: 101) } + let!(:pipeline_102) { ci_pipelines_table.create!(id: 3, partition_id: 100) } let!(:ci_pipeline_artifact_100) do ci_pipeline_artifacts_table.create!( id: 1, @@ -50,7 +50,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiPipelineArtifac file: fixture_file_upload( Rails.root.join('spec/fixtures/pipeline_artifacts/code_coverage.json'), 'application/json' ), - partition_id: pipeline_100.partition_id + partition_id: pipeline_102.partition_id ) end @@ -62,11 +62,26 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiPipelineArtifac batch_column: :id, sub_batch_size: 1, pause_ms: 0, - connection: Ci::ApplicationRecord.connection + connection: connection } end let!(:migration) { described_class.new(**migration_attrs) } + let(:connection) { Ci::ApplicationRecord.connection } + + around do |example| + connection.transaction do + connection.execute(<<~SQL) + ALTER TABLE ci_pipelines DISABLE TRIGGER ALL; + SQL + + example.run + + connection.execute(<<~SQL) + ALTER TABLE ci_pipelines ENABLE TRIGGER ALL; + SQL + end + end describe '#perform' do context 'when second partition does not exist' do @@ -79,6 +94,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillPartitionIdCiPipelineArtifac context 'when second partition exists' do before do allow(migration).to receive(:uses_multiple_partitions?).and_return(true) + pipeline_102.update!(partition_id: 101) end it 'fixes invalid records in the wrong the partition' do diff --git a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb b/spec/lib/web_ide/config/entry/global_spec.rb similarity index 91% rename from spec/lib/gitlab/web_ide/config/entry/global_spec.rb rename to spec/lib/web_ide/config/entry/global_spec.rb index 0798abf6db0..24b7ef340b4 100644 --- a/spec/lib/gitlab/web_ide/config/entry/global_spec.rb +++ b/spec/lib/web_ide/config/entry/global_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::WebIde::Config::Entry::Global do +RSpec.describe WebIde::Config::Entry::Global, feature_category: :web_ide do let(:global) { described_class.new(hash) } describe '.nodes' do @@ -38,7 +38,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do it 'creates node object using valid class' do expect(global.descendants.first) - .to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal + .to be_an_instance_of WebIde::Config::Entry::Terminal end it 'sets correct description for nodes' do @@ -108,7 +108,9 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do describe '#errors' do it 'reports errors about missing script' do expect(global.errors) - .to include "terminal:before_script config should be a string or a nested array of strings up to 10 levels deep" + .to include( + "terminal:before_script config should be a string or a nested array of strings up to 10 levels deep" + ) end end end @@ -148,7 +150,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Global do context 'when entry exists' do it 'returns correct entry' do expect(global[:terminal]) - .to be_an_instance_of Gitlab::WebIde::Config::Entry::Terminal + .to be_an_instance_of WebIde::Config::Entry::Terminal expect(global[:terminal][:before_script].value).to eq ['ls'] end end diff --git a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb b/spec/lib/web_ide/config/entry/terminal_spec.rb similarity index 92% rename from spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb rename to spec/lib/web_ide/config/entry/terminal_spec.rb index 6eb5ac1af6c..dd41daccc39 100644 --- a/spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb +++ b/spec/lib/web_ide/config/entry/terminal_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do +RSpec.describe WebIde::Config::Entry::Terminal, feature_category: :web_ide do let(:entry) { described_class.new(config, with_image_ports: true) } describe '.nodes' do @@ -35,7 +35,9 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do let(:config) do { image: { name: "ruby", ports: [80] }, - services: [{ name: "mysql", alias: "service1", ports: [81] }, { name: "mysql", alias: "service2", ports: [82] }] + services: [ + { name: "mysql", alias: "service1", ports: [81] }, { name: "mysql", alias: "service2", ports: [82] } + ] } end @@ -63,7 +65,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do end context 'when entry value is not correct' do - context 'incorrect config value type' do + context 'when incorrect config value type' do let(:config) { ['incorrect'] } describe '#errors' do diff --git a/spec/lib/gitlab/web_ide/config_spec.rb b/spec/lib/web_ide/config_spec.rb similarity index 94% rename from spec/lib/gitlab/web_ide/config_spec.rb rename to spec/lib/web_ide/config_spec.rb index 11ea6150719..a714b2d79ef 100644 --- a/spec/lib/gitlab/web_ide/config_spec.rb +++ b/spec/lib/web_ide/config_spec.rb @@ -2,19 +2,19 @@ require 'spec_helper' -RSpec.describe Gitlab::WebIde::Config do +RSpec.describe WebIde::Config, feature_category: :web_ide do let(:config) do described_class.new(yml) end context 'when config is valid' do let(:yml) do - <<-EOS + <<-YAML terminal: image: image:1.0 before_script: - gem install rspec - EOS + YAML end describe '#to_hash' do diff --git a/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb b/spec/lib/web_ide/default_oauth_application_spec.rb similarity index 97% rename from spec/lib/gitlab/web_ide/default_oauth_application_spec.rb rename to spec/lib/web_ide/default_oauth_application_spec.rb index e45fd309f78..4f051ffaafb 100644 --- a/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb +++ b/spec/lib/web_ide/default_oauth_application_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::WebIde::DefaultOauthApplication, feature_category: :web_ide do +RSpec.describe WebIde::DefaultOauthApplication, feature_category: :web_ide do let_it_be(:current_user) { create(:user) } let_it_be(:oauth_application) { create(:oauth_application, owner: nil) } diff --git a/spec/lib/gitlab/web_ide/extensions_marketplace_spec.rb b/spec/lib/web_ide/extensions_marketplace_spec.rb similarity index 96% rename from spec/lib/gitlab/web_ide/extensions_marketplace_spec.rb rename to spec/lib/web_ide/extensions_marketplace_spec.rb index 7c9ba0553a6..e4793b16af4 100644 --- a/spec/lib/gitlab/web_ide/extensions_marketplace_spec.rb +++ b/spec/lib/web_ide/extensions_marketplace_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::WebIde::ExtensionsMarketplace, feature_category: :web_ide do +RSpec.describe WebIde::ExtensionsMarketplace, feature_category: :web_ide do using RSpec::Parameterized::TableSyntax let_it_be_with_reload(:current_user) { create(:user) } @@ -25,7 +25,7 @@ RSpec.describe Gitlab::WebIde::ExtensionsMarketplace, feature_category: :web_ide with_them do it 'returns the expected value' do stub_feature_flags(web_ide_extensions_marketplace: web_ide_extensions_marketplace) - expect(::Gitlab::WebIde::DefaultOauthApplication).to receive(:feature_enabled?) + expect(::WebIde::DefaultOauthApplication).to receive(:feature_enabled?) .with(current_user).and_return(web_ide_oauth) expect(described_class.feature_enabled?(user: current_user)).to be(expectation) diff --git a/spec/models/audit_events/group_audit_event_spec.rb b/spec/models/audit_events/group_audit_event_spec.rb new file mode 100644 index 00000000000..11047e474c8 --- /dev/null +++ b/spec/models/audit_events/group_audit_event_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::AuditEvents::GroupAuditEvent, feature_category: :audit_events do + it_behaves_like 'includes ::AuditEvents::CommonModel concern' do + let_it_be(:audit_event_symbol) { :audit_events_group_audit_event } + let_it_be(:audit_event_class) { described_class } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:group_id) } + end + + describe '.by_group' do + let_it_be(:group_audit_event_1) { create(:audit_events_group_audit_event) } + let_it_be(:group_audit_event_2) { create(:audit_events_group_audit_event) } + + subject(:event) { described_class.by_group(group_audit_event_1.group_id) } + + it 'returns the correct audit event' do + expect(event).to contain_exactly(group_audit_event_1) + end + end + + describe '#root_group_entity' do + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + + context 'when root_group_entity_id is set' do + subject(:event) { described_class.new(root_group_entity_id: root_group.id) } + + it "return root_group_entity through root_group_entity_id" do + expect(event.root_group_entity).to eq(root_group) + end + end + + context "when group is nil" do + subject(:event) { described_class.new(group: nil) } + + it "return nil" do + expect(event.root_group_entity).to eq(nil) + end + end + + subject(:event) { described_class.new(group: group) } + + it "return root_group and set root_group_entity_id" do + expect(event.root_group_entity).to eq(root_group) + expect(event.root_group_entity_id).to eq(root_group.id) + end + end +end diff --git a/spec/models/audit_events/instance_audit_event_spec.rb b/spec/models/audit_events/instance_audit_event_spec.rb new file mode 100644 index 00000000000..56ef2c6a50a --- /dev/null +++ b/spec/models/audit_events/instance_audit_event_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::AuditEvents::InstanceAuditEvent, feature_category: :audit_events do + it_behaves_like 'includes ::AuditEvents::CommonModel concern' do + let_it_be(:audit_event_symbol) { :audit_events_instance_audit_event } + let_it_be(:audit_event_class) { described_class } + end +end diff --git a/spec/models/audit_events/project_audit_event_spec.rb b/spec/models/audit_events/project_audit_event_spec.rb new file mode 100644 index 00000000000..425157f4e7e --- /dev/null +++ b/spec/models/audit_events/project_audit_event_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::AuditEvents::ProjectAuditEvent, feature_category: :audit_events do + it_behaves_like 'includes ::AuditEvents::CommonModel concern' do + let_it_be(:audit_event_symbol) { :audit_events_project_audit_event } + let_it_be(:audit_event_class) { described_class } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project_id) } + end + + describe '.by_project' do + let_it_be(:project_audit_event_1) { create(:audit_events_project_audit_event) } + let_it_be(:project_audit_event_2) { create(:audit_events_project_audit_event) } + + subject(:event) { described_class.by_project(project_audit_event_1.project_id) } + + it 'returns the correct audit event' do + expect(event).to contain_exactly(project_audit_event_1) + end + end + + describe '#root_group_entity' do + let_it_be(:root_group) { create(:group) } + let_it_be(:project) { create(:project, group: root_group) } + + context 'when root_group_entity_id is set' do + subject(:event) { described_class.new(root_group_entity_id: root_group.id) } + + it "return root_group_entity through root_group_entity_id" do + expect(event.root_group_entity).to eq(root_group) + end + end + + context "when project is nil" do + subject(:event) { described_class.new(project: nil) } + + it "return nil" do + expect(event.root_group_entity).to eq(nil) + end + end + + subject(:event) { described_class.new(project: project) } + + it "return root_group and set root_group_entity_id" do + expect(event.root_group_entity).to eq(root_group) + expect(event.root_group_entity_id).to eq(root_group.id) + end + end +end diff --git a/spec/models/audit_events/user_audit_event_spec.rb b/spec/models/audit_events/user_audit_event_spec.rb new file mode 100644 index 00000000000..afc5fc9fe93 --- /dev/null +++ b/spec/models/audit_events/user_audit_event_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::AuditEvents::UserAuditEvent, feature_category: :audit_events do + it_behaves_like 'includes ::AuditEvents::CommonModel concern' do + let_it_be(:audit_event_symbol) { :audit_events_user_audit_event } + let_it_be(:audit_event_class) { described_class } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user_id) } + end + + describe '.by_user' do + let_it_be(:user_audit_event_1) { create(:audit_events_user_audit_event) } + let_it_be(:user_audit_event_2) { create(:audit_events_user_audit_event) } + + subject(:event) { described_class.by_user(user_audit_event_1.user_id) } + + it 'returns the correct audit event' do + expect(event).to contain_exactly(user_audit_event_1) + end + end + + describe '.by_username' do + let_it_be(:user_audit_event_1) { create(:audit_events_user_audit_event) } + let_it_be(:user_audit_event_2) { create(:audit_events_user_audit_event) } + + subject(:event) { described_class.by_username(user_audit_event_1.user.name) } + + before do + allow(User).to receive(:find_by_username).and_return(user_audit_event_1.user) + end + + it 'returns the correct audit event' do + expect(event).to contain_exactly(user_audit_event_1) + end + end +end diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index fdf05deafe6..a32e75e5a5c 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -51,19 +51,19 @@ RSpec.describe Ci::DailyBuildGroupReportResult, feature_category: :continuous_in project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, last_pipeline_id: new_pipeline.id, + partition_id: new_pipeline.partition_id, date: rspec_coverage.date, group_name: 'rspec', - data: { 'coverage' => 81.0 }, - partition_id: 100 + data: { 'coverage' => 81.0 } }, { project_id: rspec_coverage.project_id, ref_path: rspec_coverage.ref_path, last_pipeline_id: new_pipeline.id, + partition_id: new_pipeline.partition_id, date: rspec_coverage.date, group_name: 'karma', - data: { 'coverage' => 87.0 }, - partition_id: 100 + data: { 'coverage' => 87.0 } } ]) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index f582abfaa17..f404db2697c 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -435,6 +435,16 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do let_it_be(:namespace1sub) { create(:group, name: 'Sub Namespace', path: 'sub-namespace', parent: namespace1) } let_it_be(:namespace2sub) { create(:group, name: 'Sub Namespace', path: 'sub-namespace', parent: namespace2) } + describe '.without_deleted' do + before do + namespace1.namespace_details.update!(pending_delete: true) + end + + it 'does not include namespace marked as deleted' do + expect(described_class.without_deleted).to contain_exactly(namespace, namespace2, namespace1sub, namespace2sub) + end + end + describe '.by_parent' do it 'includes correct namespaces' do expect(described_class.by_parent(namespace1.id)).to match_array([namespace1sub]) @@ -550,6 +560,8 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do it { is_expected.to delegate_method(:math_rendering_limits_enabled?).to(:namespace_settings) } it { is_expected.to delegate_method(:lock_math_rendering_limits_enabled?).to(:namespace_settings) } it { is_expected.to delegate_method(:add_creator).to(:namespace_details) } + it { is_expected.to delegate_method(:pending_delete).to(:namespace_details) } + it { is_expected.to delegate_method(:pending_delete=).to(:namespace_details).with_arguments(:args) } it do is_expected.to delegate_method(:prevent_sharing_groups_outside_hierarchy=).to(:namespace_settings) diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb index 765d5157586..a86b61ef1b4 100644 --- a/spec/requests/api/graphql/gitlab_schema_spec.rb +++ b/spec/requests/api/graphql/gitlab_schema_spec.rb @@ -196,8 +196,8 @@ RSpec.describe 'GitlabSchema configurations', feature_category: :integrations do hash_including( trace_type: 'execute_query', "query_analysis.duration_s" => 7, - "query_analysis.complexity" => 181, - "query_analysis.depth" => 13, + "query_analysis.complexity" => 217, + "query_analysis.depth" => 15, "query_analysis.used_deprecated_fields" => an_instance_of(Array), "query_analysis.used_deprecated_arguments" => an_instance_of(Array), "query_analysis.used_fields" => an_instance_of(Array) diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index ebdce07d03c..9c5fd6b04f9 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -86,8 +86,17 @@ RSpec.describe Groups::DestroyService, feature_category: :groups_and_projects do end end + shared_examples 'marks the group as pending delete' do |async| + specify do + expect(group).to receive(:update_attribute).with(:pending_delete, true) + + destroy_group(group, user, async) + end + end + describe 'asynchronous delete' do it_behaves_like 'group destruction', true + it_behaves_like 'marks the group as pending delete', true context 'Sidekiq fake' do before do @@ -105,6 +114,19 @@ RSpec.describe Groups::DestroyService, feature_category: :groups_and_projects do describe 'synchronous delete' do it_behaves_like 'group destruction', false + it_behaves_like 'marks the group as pending delete', false + + context 'when destroying the group throws an error' do + before do + allow(group).to receive(:destroy).and_raise(StandardError) + end + + it 'unmarks the group as pending delete' do + expect { destroy_group(group, user, false) }.to raise_error(StandardError) + + expect(group.pending_delete).to be_falsey + end + end end context 'projects in pending_delete' do diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index beef5f5175b..37800467bc8 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -1092,7 +1092,7 @@ - './ee/spec/lib/ee/gitlab/verify/lfs_objects_spec.rb' - './ee/spec/lib/ee/gitlab/verify/uploads_spec.rb' - './ee/spec/lib/ee/gitlab/web_hooks/rate_limiter_spec.rb' -- './ee/spec/lib/ee/gitlab/web_ide/config/entry/global_spec.rb' +- './ee/spec/lib/ee/web_ide/config/entry/global_spec.rb' - './ee/spec/lib/ee/service_ping/build_payload_spec.rb' - './ee/spec/lib/ee/service_ping/permit_data_categories_spec.rb' - './ee/spec/lib/ee/service_ping/service_ping_settings_spec.rb' @@ -1411,10 +1411,10 @@ - './ee/spec/lib/gitlab/usage/metrics/instrumentations/user_cap_setting_enabled_metric_spec.rb' - './ee/spec/lib/gitlab/user_access_spec.rb' - './ee/spec/lib/gitlab/visibility_level_spec.rb' -- './ee/spec/lib/gitlab/web_ide/config/entry/schema/match_spec.rb' -- './ee/spec/lib/gitlab/web_ide/config/entry/schema_spec.rb' -- './ee/spec/lib/gitlab/web_ide/config/entry/schemas_spec.rb' -- './ee/spec/lib/gitlab/web_ide/config/entry/schema/uri_spec.rb' +- './ee/spec/lib/web_ide/config/entry/schema/match_spec.rb' +- './ee/spec/lib/web_ide/config/entry/schema_spec.rb' +- './ee/spec/lib/web_ide/config/entry/schemas_spec.rb' +- './ee/spec/lib/web_ide/config/entry/schema/uri_spec.rb' - './ee/spec/lib/omni_auth/strategies/group_saml_spec.rb' - './ee/spec/lib/omni_auth/strategies/kerberos_spec.rb' - './ee/spec/lib/peek/views/elasticsearch_spec.rb' @@ -6067,9 +6067,9 @@ - './spec/lib/gitlab/visibility_level_spec.rb' - './spec/lib/gitlab/web_hooks/rate_limiter_spec.rb' - './spec/lib/gitlab/web_hooks/recursion_detection_spec.rb' -- './spec/lib/gitlab/web_ide/config/entry/global_spec.rb' -- './spec/lib/gitlab/web_ide/config/entry/terminal_spec.rb' -- './spec/lib/gitlab/web_ide/config_spec.rb' +- './spec/lib/web_ide/config/entry/global_spec.rb' +- './spec/lib/web_ide/config/entry/terminal_spec.rb' +- './spec/lib/web_ide/config_spec.rb' - './spec/lib/gitlab/webpack/file_loader_spec.rb' - './spec/lib/gitlab/webpack/graphql_known_operations_spec.rb' - './spec/lib/gitlab/webpack/manifest_spec.rb' diff --git a/spec/support/shared_examples/models/concerns/audit_events/common_model_shared_examples.rb b/spec/support/shared_examples/models/concerns/audit_events/common_model_shared_examples.rb new file mode 100644 index 00000000000..1069c6ba6ef --- /dev/null +++ b/spec/support/shared_examples/models/concerns/audit_events/common_model_shared_examples.rb @@ -0,0 +1,310 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'includes ::AuditEvents::CommonModel concern' do + describe 'associations' do + it { is_expected.to belong_to(:user).with_foreign_key(:author_id).inverse_of(:audit_events) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:author_id) } + + include_examples 'validates IP address' do + let(:attribute) { :ip_address } + let(:object) { create(audit_event_symbol) } # rubocop:disable Rails/SaveBang -- Method not available + end + end + + describe 'callbacks' do + describe '#truncate_fields' do + shared_examples 'a truncated field' do + context 'when values are provided' do + using RSpec::Parameterized::TableSyntax + + where(:database_column, :details_value, :expected_value) do + :long | nil | :truncated + :short | nil | :short + nil | :long | :truncated + nil | :short | :short + :long | :short | :truncated + end + + with_them do + let(:values) do + { + long: 'a' * (field_limit + 1), + short: 'a' * field_limit, + truncated: "#{'a' * (field_limit - 3)}..." + } + end + + let(:audit_event) do + create(audit_event_symbol, + field_name => values[database_column], + details: { field_name => values[details_value] } + ) + end + + it 'sets both values to be the same', :aggregate_failures do + expect(audit_event.send(field_name)).to eq(values[expected_value]) + expect(audit_event.details[field_name]).to eq(values[expected_value]) + end + end + end + + context 'when values are not provided' do + let(:audit_event) do + create(:audit_event, field_name => nil, details: {}) + end + + it 'does not set', :aggregate_failures do + expect(audit_event.send(field_name)).to be_nil + expect(audit_event.details).not_to have_key(field_name) + end + end + end + + context 'for entity_path' do + let(:field_name) { :entity_path } + let(:field_limit) { 5_500 } + + it_behaves_like 'a truncated field' + end + + context 'for target_details' do + let(:field_name) { :target_details } + let(:field_limit) { 5_500 } + + it_behaves_like 'a truncated field' + end + end + + describe '#parallel_persist' do + shared_examples 'a parallel persisted field' do + using RSpec::Parameterized::TableSyntax + + where(:column, :details, :expected_value) do + :value | nil | :value + nil | :value | :value + :value | :another_value | :value + nil | nil | nil + end + + with_them do + let(:values) { { value: value, another_value: "#{value}88" } } + + let(:audit_event) do + build(audit_event_symbol, name => values[column], details: { name => values[details] }) + end + + it 'sets both values to be the same', :aggregate_failures do + audit_event.validate + + expect(audit_event[name]).to eq(values[expected_value]) + expect(audit_event.details[name]).to eq(values[expected_value]) + end + end + end + + context 'with author_name' do + let(:name) { :author_name } + let(:value) { 'Mary Poppins' } + + it_behaves_like 'a parallel persisted field' + end + + context 'with target_details' do + let(:name) { :target_details } + let(:value) { 'gitlab-org/gitlab' } + + it_behaves_like 'a parallel persisted field' + end + + context 'with target_type' do + let(:name) { :target_type } + let(:value) { 'Project' } + + it_behaves_like 'a parallel persisted field' + end + + context 'with target_id' do + let(:name) { :target_id } + let(:value) { 8 } + + it_behaves_like 'a parallel persisted field' + end + end + end + + describe '.order_by' do + let_it_be(:event_1) { create(audit_event_symbol) } # rubocop:disable Rails/SaveBang -- Method not available + let_it_be(:event_2) { create(audit_event_symbol) } # rubocop:disable Rails/SaveBang -- Method not available + let_it_be(:event_3) { create(audit_event_symbol) } # rubocop:disable Rails/SaveBang -- Method not available + + subject(:events) { audit_event_class.order_by(method) } + + context 'when sort by created_at in ascending order' do + let(:method) { 'created_asc' } + + it 'sorts results by id in ascending order' do + expect(events).to eq([event_1, event_2, event_3]) + end + end + + context 'when it is default' do + let(:method) { nil } + + it 'sorts results by id in descending order' do + expect(events.count).to eq(3) + expect(events).to eq([event_3, event_2, event_1]) + end + end + end + + it 'sanitizes custom_message in the details hash' do + audit_event = create(audit_event_symbol, details: { target_id: 678, custom_message: 'Arnold' }) + + expect(audit_event.details).to include( + target_id: 678, + custom_message: 'Arnold' + ) + end + + describe '#as_json' do + context 'for ip_address' do + subject { build(audit_event_symbol, ip_address: '192.168.1.1').as_json } + + it 'overrides the ip_address with its string value' do + expect(subject['ip_address']).to eq('192.168.1.1') + end + end + end + + describe '#author_name' do + context 'when user exists' do + let(:user) { create(:user, name: 'John Doe') } + + subject(:event) { audit_event_class.new(user: user) } + + it 'returns user name' do + expect(event.author_name).to eq 'John Doe' + end + end + + context 'when user does not exist anymore' do + context 'when database contains author_name' do + subject(:event) { build(audit_event_symbol, author_id: non_existing_record_id, author_name: 'Jane Doe') } + + it 'returns author_name' do + expect(event.author_name).to eq 'Jane Doe' + end + end + + context 'when details contains author_name' do + subject(:event) do + build(audit_event_symbol, author_id: non_existing_record_id, author_name: nil, + details: { author_name: 'John Doe' }) + end + + it 'returns author_name' do + expect(event.author_name).to eq 'John Doe' + end + end + + context 'when details does not contains author_name' do + subject(:event) do + build(audit_event_symbol, author_name: nil, + details: {}) + end + + it 'returns nil' do + expect(subject.author_name).to eq nil + end + end + end + + context 'when authored by an unauthenticated user' do + subject(:event) { build(audit_event_symbol, author_name: nil, details: {}, author_id: -1) } + + it 'returns `An unauthenticated user`' do + expect(subject.author_name).to eq('An unauthenticated user') + end + end + end + + describe '#ip_address' do + context 'when ip_address exists in both details hash and ip_address column' do + subject(:event) do + build(audit_event_symbol, ip_address: '10.2.1.1', details: { ip_address: '192.168.0.1' }) + end + + it 'returns the value from ip_address column' do + expect(event.ip_address).to eq('10.2.1.1') + end + end + + context 'when ip_address exists in details hash but not in ip_address column' do + subject(:event) { build(audit_event_symbol, ip_address: nil, details: { ip_address: '192.168.0.1' }) } + + it 'returns the value from details hash' do + expect(event.ip_address).to eq('192.168.0.1') + end + end + end + + describe '#entity_path' do + context 'when entity_path exists in both details hash and entity_path column' do + subject(:event) do + build(audit_event_symbol, entity_path: 'gitlab-org/gitlab', details: { entity_path: 'gitlab-org/gitlab-foss' }) + end + + it 'returns the value from entity_path column' do + expect(event.entity_path).to eq('gitlab-org/gitlab') + end + end + + context 'when entity_path exists in details hash but not in entity_path column' do + subject(:event) do + build(audit_event_symbol, entity_path: nil, details: { entity_path: 'gitlab-org/gitlab-foss' }) + end + + it 'returns the value from details hash' do + expect(event.entity_path).to eq('gitlab-org/gitlab-foss') + end + end + end + + describe '#target_type' do + context 'when target_type exists in both details hash and target_type column' do + subject(:event) do + build(audit_event_symbol, target_type: 'Group', details: { target_type: 'Project' }) + end + + it 'returns the value from target_type column' do + expect(event.target_type).to eq('Group') + end + end + + context 'when target_type exists in details hash but not in target_type column' do + subject(:event) { build(audit_event_symbol, details: { target_type: 'Project' }) } + + it 'returns the value from details hash' do + expect(event.target_type).to eq('Project') + end + end + end + + describe '#formatted_details' do + subject(:event) do + create(audit_event_symbol, details: { change: 'membership_lock', from: false, to: true, ip_address: '127.0.0.1' }) + end + + it 'includes the author\'s email' do + expect(event.formatted_details[:author_email]).to eq(event.author.email) + end + + it 'converts value of `to` and `from` in `details` to string' do + expect(event.formatted_details[:to]).to eq('true') + expect(event.formatted_details[:from]).to eq('false') + end + end +end