diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f20674699f7..e2e950f7790 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -259,8 +259,17 @@ export default { ); } }, + expandedPanel: { + handler({ group, panel }) { + const dashboardPath = this.currentDashboard || this.firstDashboard.path; + updateHistory({ + url: panelToUrl(dashboardPath, group, panel), + title: document.title, + }); + }, + deep: true, + }, }, - created() { this.setInitialState({ metricsEndpoint: this.metricsEndpoint, diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 4f90294ee3a..40f5eb765b4 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -1,4 +1,3 @@ -import { pickBy } from 'lodash'; import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; import { timeRangeParamNames, @@ -174,25 +173,30 @@ export const expandedPanelPayloadFromUrl = (dashboard, search = window.location. * Convert panel information to a URL for the user to * bookmark or share highlighting a specific panel. * - * @param {String} dashboardPath - Dashboard path used as identifier - * @param {String} group - Group Identifier + * If no group/panel is set, the dashboard URL is returned. + * + * @param {?String} dashboard - Dashboard path, used as identifier for a dashboard + * @param {?String} group - Group Identifier * @param {?Object} panel - Panel object from the dashboard * @param {?String} url - Base URL including current search params * @returns Dashboard URL which expands a panel (chart) */ -export const panelToUrl = (dashboardPath, group, panel, url = window.location.href) => { - if (!group || !panel) { - return null; +export const panelToUrl = (dashboard = null, group, panel, url = window.location.href) => { + const params = { + dashboard, + }; + + if (group && panel) { + params.group = group; + params.title = panel.title; + params.y_label = panel.y_label; + } else { + // Remove existing parameters if any + params.group = null; + params.title = null; + params.y_label = null; } - const params = pickBy( - { - dashboard: dashboardPath, - group, - title: panel.title, - y_label: panel.y_label, - }, - value => value != null, - ); + return mergeUrlParams(params, url); }; diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index a0ab00f0f07..656a567c0a7 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -683,6 +683,8 @@ module Ci variables.concat(merge_request.predefined_variables) end + variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active? + if external_pull_request_event? && external_pull_request variables.concat(external_pull_request.predefined_variables) end diff --git a/app/serializers/accessibility_error_entity.rb b/app/serializers/accessibility_error_entity.rb new file mode 100644 index 00000000000..540f5384d66 --- /dev/null +++ b/app/serializers/accessibility_error_entity.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class AccessibilityErrorEntity < Grape::Entity + expose :code + expose :type + expose :typeCode, as: :type_code + expose :message + expose :context + expose :selector + expose :runner + expose :runnerExtras, as: :runner_extras +end diff --git a/app/serializers/accessibility_reports_comparer_entity.rb b/app/serializers/accessibility_reports_comparer_entity.rb new file mode 100644 index 00000000000..3768607a3fc --- /dev/null +++ b/app/serializers/accessibility_reports_comparer_entity.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AccessibilityReportsComparerEntity < Grape::Entity + expose :status + + expose :new_errors, using: AccessibilityErrorEntity + expose :resolved_errors, using: AccessibilityErrorEntity + expose :existing_errors, using: AccessibilityErrorEntity + + expose :summary do + expose :total_count, as: :total + expose :resolved_count, as: :resolved + expose :errors_count, as: :errored + end +end diff --git a/app/serializers/accessibility_reports_comparer_serializer.rb b/app/serializers/accessibility_reports_comparer_serializer.rb new file mode 100644 index 00000000000..a6b8162e4ea --- /dev/null +++ b/app/serializers/accessibility_reports_comparer_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class AccessibilityReportsComparerSerializer < BaseSerializer + entity AccessibilityReportsComparerEntity +end diff --git a/changelogs/unreleased/215473-url-update-single-panel.yml b/changelogs/unreleased/215473-url-update-single-panel.yml new file mode 100644 index 00000000000..730c35ddce0 --- /dev/null +++ b/changelogs/unreleased/215473-url-update-single-panel.yml @@ -0,0 +1,5 @@ +--- +title: Update metrics dashboard url when a panel is expanded or contracted +merge_request: 30704 +author: +type: added diff --git a/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml b/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml new file mode 100644 index 00000000000..d8aa1b0e0e9 --- /dev/null +++ b/changelogs/unreleased/add-ci-kubernetes-active-pipeline-variable.yml @@ -0,0 +1,6 @@ +--- +title: Add a CI variable CI_KUBERNETES_ACTIVE as an alternative to only:kubernetes/except:kubernetes + that works with the rules syntax +merge_request: 31146 +author: +type: added diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md index 6f417f955ac..a73a79c4862 100644 --- a/doc/administration/geo/disaster_recovery/index.md +++ b/doc/administration/geo/disaster_recovery/index.md @@ -167,6 +167,44 @@ do this manually. previously for the **secondary**. 1. Success! The **secondary** has now been promoted to **primary**. +#### Promoting a **secondary** node with an external PostgreSQL database + +The `gitlab-ctl promote-to-primary-node` command cannot be used in conjunction with +an external PostgreSQL database, as it can only perform changes on a **secondary** +node with GitLab and the database on the same machine. As a result, a manual process is +required. For example, PostgreSQL databases hosted on Amazon RDS: + +1. Promote the replica database associated with the **secondary** site. This will + set the database to read-write: + - Amazon RDS - [Promoting a Read Replica to Be a Standalone DB Instance](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_ReadRepl.html#USER_ReadRepl.Promote) + +1. Edit `/etc/gitlab/gitlab.rb` on every node in the **secondary** site to + reflect its new status as **primary** by removing any lines that enabled the + `geo_secondary_role`: + + ```ruby + ## In GitLab 11.4 and earlier, remove this line. + geo_secondary_role['enable'] = true + + ## In GitLab 11.5 and later, remove this line. + roles ['geo_secondary_role'] + ``` + + After making these changes [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) + on each node so the changes take effect. + +1. Promote the **secondary** to **primary**. SSH into a single secondary application + node and execute: + + ```shell + sudo gitlab-rake geo:set_secondary_as_primary + ``` + +1. Verify you can connect to the newly promoted **primary** site using the URL used + previously for the **secondary** site. + +Success! The **secondary** site has now been promoted to **primary**. + ### Step 4. (Optional) Updating the primary domain DNS record Updating the DNS records for the primary domain to point to the **secondary** node diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md index a0754e94703..d4d3a13bb2a 100644 --- a/doc/ci/variables/predefined_variables.md +++ b/doc/ci/variables/predefined_variables.md @@ -64,6 +64,7 @@ future GitLab releases.** | `CI_JOB_TOKEN` | 9.0 | 1.2 | Token used for authenticating with the [GitLab Container Registry](../../user/packages/container_registry/index.md) and downloading [dependent repositories](../../user/project/new_ci_build_permissions_model.md#dependent-repositories) | | `CI_JOB_JWT` | 12.10 | all | RS256 JSON web token that can be used for authenticating with third party systems that support JWT authentication, for example [HashiCorp's Vault](../examples/authenticating-with-hashicorp-vault). | | `CI_JOB_URL` | 11.1 | 0.5 | Job details URL | +| `CI_KUBERNETES_ACTIVE` | 13.0 | all | Included with the value `true` only if the pipeline has a Kubernetes cluster available for deployments. Not included if no cluster is availble. Can be used as an alternative to [`only:kubernetes`/`except:kubernetes`](../yaml/README.md#onlykubernetesexceptkubernetes) with [`rules:if`](../yaml/README.md#rulesif) | | `CI_MERGE_REQUEST_ASSIGNEES` | 11.9 | all | Comma-separated list of username(s) of assignee(s) for the merge request if [the pipelines are for merge requests](../merge_request_pipelines/index.md). Available only if `only: [merge_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the merge request is created. | | `CI_MERGE_REQUEST_CHANGED_PAGE_PATHS` | 12.9 | all | Comma-separated list of paths of changed pages in a deployed [Review App](../review_apps/index.md) for a [Merge Request](../merge_request_pipelines/index.md). A [Route Map](../review_apps/index.md#route-maps) must be configured. | | `CI_MERGE_REQUEST_CHANGED_PAGE_URLS` | 12.9 | all | Comma-separated list of URLs of changed pages in a deployed [Review App](../review_apps/index.md) for a [Merge Request](../merge_request_pipelines/index.md). A [Route Map](../review_apps/index.md#route-maps) must be configured. | diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 9ece6eff41e..c5cb699317e 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -171,8 +171,39 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be done without requiring downtime. However, this does require that any application changes are deployed _first_. Thus, changing the constraints of a column should happen in a post-deployment migration. -NOTE: Avoid using `change_column` as it produces inefficient query because it re-defines -the whole column type. For example, to add a NOT NULL constraint, prefer `change_column_null` + +NOTE: Avoid using `change_column` as it produces an inefficient query because it re-defines +the whole column type. + +To add a NOT NULL constraint, use the `add_not_null_constraint` migration helper: + +```ruby +# A post-deployment migration in db/post_migrate +class AddNotNull < ActiveRecord::Migration[4.2] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + def up + add_not_null_constraint :users, :username + end + + def down + remove_not_null_constraint :users, :username + end +end +``` + +If the column to be updated requires cleaning first (e.g. there are `NULL` values), you should: + +1. Add the `NOT NULL` constraint with `validate: false` + + `add_not_null_constraint :users, :username, validate: false` + +1. Clean up the data with a data migration +1. Validate the `NOT NULL` constraint with a followup migration + + `validate_not_null_constraint :users, :username` ## Changing Column Types diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 60d189c8b42..781ad923610 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -338,3 +338,77 @@ To fix this issue, you can either: [Learn more on overriding the SAST template](sast/index.md#overriding-the-sast-template). All the security scanning tools define their stage, so this error can occur with all of them. + +### Getting error message `sast job: config key may not be used with 'rules': only/except` + +When including a security job template like [`SAST`](sast/index.md#overriding-the-sast-template), +the following error may occur, depending on your GitLab CI/CD configuration: + +```plaintext +Found errors in your .gitlab-ci.yml: + + jobs:sast config key may not be used with `rules`: only/except +``` + +This error appears when the included job's `rules` configuration has been [overridden](sast/index.md#overriding-the-sast-template) +with [the deprecated `only` or `except` syntax.](../../ci/yaml/README.md#onlyexcept-basic) +To fix this issue, you must either: + +- [Transition your `only/except` syntax to `rules`](#transitioning-your-onlyexcept-syntax-to-rules). +- (Temporarily) [Pin your templates to the deprecated versions](#pin-your-templates-to-the-deprecated-versions) + +[Learn more on overriding the SAST template](sast/index.md#overriding-the-sast-template). + +#### Transitioning your `only/except` syntax to `rules` + +When overriding the template to control job execution, previous instances of +[`only` or `except`](../../ci/yaml/README.md#onlyexcept-basic) are no longer compatible +and must be transitioned to [the `rules` syntax](../../ci/yaml/README.md#rules). + +If your override is aimed at limiting jobs to only run on `master`, the previous syntax +would look similar to: + +```yaml +include: + - template: SAST.gitlab-ci.yml + +# Ensure that the scanning is only executed on master or merge requests +spotbugs-sast: + only: + refs: + - master + - merge_requests +``` + +To transition the above configuration to the new `rules` syntax, the override +would be written as follows: + +```yaml +include: + - template: SAST.gitlab-ci.yml + +# Ensure that the scanning is only executed on master or merge requests +spotbugs-sast: + rules: + - if: $CI_COMMIT_BRANCH == "master" + - if: $CI_MERGE_REQUEST_ID +``` + +[Learn more on the usage of `rules`](../../ci/yaml/README.md#rules). + +#### Pin your templates to the deprecated versions + +To ensure the latest support, we **strongly** recommend that you migrate to [`rules`](../../ci/yaml/README.md#rules). + +If you're unable to immediately update your CI configuration, there are several workarounds that +involve pinning to the previous template versions, for example: + + ```yaml + include: + remote: 'https://gitlab.com/gitlab-org/gitlab/-/raw/12-10-stable-ee/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml' + ``` + +Additionally, we provide a dedicated project containing the versioned legacy templates. +This can be useful for offline setups or anyone wishing to use [Auto DevOps](../../topics/autodevops/index.md).. + +Instructions are available in the [legacy template project](https://gitlab.com/gitlab-org/auto-devops-v12-10). diff --git a/lib/gitlab/ci/parsers/accessibility/pa11y.rb b/lib/gitlab/ci/parsers/accessibility/pa11y.rb index 57eca184c33..953b5a91258 100644 --- a/lib/gitlab/ci/parsers/accessibility/pa11y.rb +++ b/lib/gitlab/ci/parsers/accessibility/pa11y.rb @@ -6,7 +6,7 @@ module Gitlab module Accessibility class Pa11y def parse!(json_data, accessibility_report) - root = Gitlab::Json.parse(json_data) + root = Gitlab::Json.parse(json_data).with_indifferent_access parse_all(root, accessibility_report) rescue JSON::ParserError => e diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 710da8282ff..bf14bd4ab4b 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -265,12 +265,19 @@ module Gitlab # or `RESET ALL` is executed def disable_statement_timeout if block_given? - begin - execute('SET statement_timeout TO 0') - + if statement_timeout_disabled? + # Don't do anything if the statement_timeout is already disabled + # Allows for nested calls of disable_statement_timeout without + # resetting the timeout too early (before the outer call ends) yield - ensure - execute('RESET ALL') + else + begin + execute('SET statement_timeout TO 0') + + yield + ensure + execute('RESET ALL') + end end else unless transaction_open? @@ -495,7 +502,7 @@ module Gitlab update_column_in_batches(table, column, default_after_type_cast, &block) end - change_column_null(table, column, false) unless allow_null + add_not_null_constraint(table, column) unless allow_null # We want to rescue _all_ exceptions here, even those that don't inherit # from StandardError. rescue Exception => error # rubocop: disable all @@ -1334,12 +1341,73 @@ into similar problems in the future (e.g. when new tables are created). check_constraint_exists?(table, text_limit_name(table, column, name: constraint_name)) end + # Migration Helpers for managing not null constraints + def add_not_null_constraint(table, column, constraint_name: nil, validate: true) + if column_is_nullable?(table, column) + add_check_constraint( + table, + "#{column} IS NOT NULL", + not_null_constraint_name(table, column, name: constraint_name), + validate: validate + ) + else + warning_message = <<~MESSAGE + NOT NULL check constraint was not created: + column #{table}.#{column} is already defined as `NOT NULL` + MESSAGE + + Rails.logger.warn warning_message + end + end + + def validate_not_null_constraint(table, column, constraint_name: nil) + validate_check_constraint( + table, + not_null_constraint_name(table, column, name: constraint_name) + ) + end + + def remove_not_null_constraint(table, column, constraint_name: nil) + remove_check_constraint( + table, + not_null_constraint_name(table, column, name: constraint_name) + ) + end + + def check_not_null_constraint_exists?(table, column, constraint_name: nil) + check_constraint_exists?( + table, + not_null_constraint_name(table, column, name: constraint_name) + ) + end + private + def statement_timeout_disabled? + # This is a string of the form "100ms" or "0" when disabled + connection.select_value('SHOW statement_timeout') == "0" + end + + def column_is_nullable?(table, column) + # Check if table.column has not been defined with NOT NULL + check_sql = <<~SQL + SELECT c.is_nullable + FROM information_schema.columns c + WHERE c.table_name = '#{table}' + AND c.column_name = '#{column}' + SQL + + connection.select_value(check_sql) == 'YES' + end + def text_limit_name(table, column, name: nil) name.presence || check_constraint_name(table, column, 'max_length') end + def not_null_constraint_name(table, column, name: nil) + name.presence || check_constraint_name(table, column, 'not_null') + end + def missing_schema_object_message(table, type, name) <<~MESSAGE Could not find #{type} "#{name}" on table "#{table}" which was referenced during the migration. @@ -1383,7 +1451,7 @@ into similar problems in the future (e.g. when new tables are created). update_column_in_batches(table, new, Arel::Table.new(table)[old], batch_column_name: batch_column_name) - change_column_null(table, new, false) unless old_col.null + add_not_null_constraint(table, new) unless old_col.null copy_indexes(table, old, new) copy_foreign_keys(table, old, new) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb index 13fe8918f97..d0123da53bb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do - describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do + context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217002', type: :investigating } do + describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } let(:parent_project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb index 8ea1534492c..ed988bdf046 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Create', quarantine: { type: :new } do + context 'Create' do describe 'Review a merge request in Web IDE' do let(:new_file) { 'awesome_new_file.txt' } let(:original_text) { 'Text' } diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb index e71212bcb68..b78aee75471 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Release', :docker, quarantine: { type: :new } do + context 'Release', :docker do describe 'Parent-child pipelines dependent relationship' do let!(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb index 633af9c2e8a..bd5e66b8669 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Release', :docker, quarantine: { type: :new } do + context 'Release', :docker do describe 'Parent-child pipelines independent relationship' do let!(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb index ab6c08f8ec5..04c68598239 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context 'Configure' do - describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, quarantine: { type: :new } do + context 'Configure', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/209085', type: :investigating } do + describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin do context 'Project Clusters' do let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } let(:project) do diff --git a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb index 465b3530d00..da63b1ac7c8 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb @@ -2,7 +2,7 @@ module QA context 'Monitor' do - describe 'Dashboards', :orchestrated, :kubernetes, quarantine: { type: :new } do + describe 'Dashboards', :orchestrated, :kubernetes, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29262', type: :waiting_on } do before(:all) do @cluster = Service::KubernetesCluster.new.create! Flow::Login.sign_in diff --git a/spec/fixtures/api/schemas/entities/accessibility_error.json b/spec/fixtures/api/schemas/entities/accessibility_error.json new file mode 100644 index 00000000000..3ea84835505 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/accessibility_error.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "required": [ + "code", + "type", + "type_code", + "message", + "context", + "selector", + "runner", + "runner_extras" + ], + "properties": { + "code": { + "type": "string" + }, + "type": { + "type": "string" + }, + "type_code": { + "type": "integer" + }, + "message": { + "type": "string" + }, + "context": { + "type": "string" + }, + "selector": { + "type": "string" + }, + "runner": { + "type": "string" + }, + "runner_extras": { + "type": ["object", "null"] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json b/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json new file mode 100644 index 00000000000..ec243354eab --- /dev/null +++ b/spec/fixtures/api/schemas/entities/accessibility_reports_comparer.json @@ -0,0 +1,43 @@ +{ + "type": "object", + "required": ["status", "summary", "new_errors", "resolved_errors", "existing_errors"], + "properties": { + "status": { + "type": "string" + }, + "summary": { + "type": "object", + "properties": { + "total": { + "type": "integer" + }, + "resolved": { + "type": "integer" + }, + "errored": { + "type": "integer" + } + }, + "required": ["total", "resolved", "errored"] + }, + "new_errors": { + "type": "array", + "items": { + "$ref": "accessibility_error.json" + } + }, + "resolved_errors": { + "type": "array", + "items": { + "$ref": "accessibility_error.json" + } + }, + "existing_errors": { + "type": "array", + "items": { + "$ref": "accessibility_error.json" + } + } + }, + "additionalProperties": false +} diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index 12cb2c588dd..78553999705 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -234,6 +234,90 @@ describe('Dashboard', () => { }); }); + describe('when the panel is expanded', () => { + let group; + let panel; + + const expandPanel = (mockGroup, mockPanel) => { + store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, { + group: mockGroup, + panel: mockPanel, + }); + }; + + beforeEach(() => { + setupStoreWithData(store); + + const { panelGroups } = store.state.monitoringDashboard.dashboard; + group = panelGroups[0].group; + [panel] = panelGroups[0].panels; + + jest.spyOn(window.history, 'pushState').mockImplementation(); + }); + + afterEach(() => { + window.history.pushState.mockRestore(); + }); + + it('URL is updated with panel parameters', () => { + createMountedWrapper({ hasMetrics: true }); + expandPanel(group, panel); + + const expectedSearch = objectToQuery({ + group, + title: panel.title, + y_label: panel.y_label, + }); + + return wrapper.vm.$nextTick(() => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), // state + expect.any(String), // document title + expect.stringContaining(`?${expectedSearch}`), + ); + }); + }); + + it('URL is updated with panel parameters and custom dashboard', () => { + const dashboard = 'dashboard.yml'; + + createMountedWrapper({ hasMetrics: true, currentDashboard: dashboard }); + expandPanel(group, panel); + + const expectedSearch = objectToQuery({ + dashboard, + group, + title: panel.title, + y_label: panel.y_label, + }); + + return wrapper.vm.$nextTick(() => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), // state + expect.any(String), // document title + expect.stringContaining(`?${expectedSearch}`), + ); + }); + }); + + it('URL is updated with no parameters', () => { + expandPanel(group, panel); + createMountedWrapper({ hasMetrics: true }); + expandPanel(null, null); + + return wrapper.vm.$nextTick(() => { + expect(window.history.pushState).toHaveBeenCalledTimes(1); + expect(window.history.pushState).toHaveBeenCalledWith( + expect.anything(), // state + expect.any(String), // document title + expect.not.stringContaining('?'), // no params + ); + }); + }); + }); + describe('when all requests have been commited by the store', () => { beforeEach(() => { createMountedWrapper({ hasMetrics: true }); diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 639330446be..b7e34853552 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -274,9 +274,10 @@ describe('monitoring/utils', () => { const [panelGroup] = metricsDashboardViewModel.panelGroups; const [panel] = panelGroup.panels; + const getUrlParams = url => urlUtils.queryToObject(url.split('?')[1]); + it('returns URL for a panel when query parameters are given', () => { - const [, query] = panelToUrl(dashboard, panelGroup.group, panel).split('?'); - const params = urlUtils.queryToObject(query); + const params = getUrlParams(panelToUrl(dashboard, panelGroup.group, panel)); expect(params).toEqual({ dashboard, @@ -286,12 +287,14 @@ describe('monitoring/utils', () => { }); }); - it('returns `null` if group is missing', () => { - expect(panelToUrl(dashboard, null, panel)).toBe(null); + it('returns a dashboard only URL if group is missing', () => { + const params = getUrlParams(panelToUrl(dashboard, null, panel)); + expect(params).toEqual({ dashboard: 'metrics.yml' }); }); - it('returns `null` if panel is missing', () => { - expect(panelToUrl(dashboard, panelGroup.group, null)).toBe(null); + it('returns a dashboard only URL if panel is missing', () => { + const params = getUrlParams(panelToUrl(dashboard, panelGroup.group, null)); + expect(params).toEqual({ dashboard: 'metrics.yml' }); }); }); diff --git a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb index e0f1dc084d5..4d87e3b201a 100644 --- a/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb +++ b/spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb @@ -88,6 +88,7 @@ describe Gitlab::Ci::Parsers::Accessibility::Pa11y do expect(accessibility_report.passes_count).to eq(0) expect(accessibility_report.scans_count).to eq(1) expect(accessibility_report.urls['https://about.gitlab.com/']).to be_present + expect(accessibility_report.urls['https://about.gitlab.com/'].first[:code]).to be_present end end end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 46181038445..73b9dcc5bf9 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -217,9 +217,10 @@ describe Gitlab::Database::MigrationHelpers do it 'appends ON DELETE SET NULL statement' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) expect(model).to receive(:execute).with(/ON DELETE SET NULL/) @@ -233,9 +234,10 @@ describe Gitlab::Database::MigrationHelpers do it 'appends ON DELETE CASCADE statement' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) expect(model).to receive(:execute).with(/ON DELETE CASCADE/) @@ -249,9 +251,10 @@ describe Gitlab::Database::MigrationHelpers do it 'appends no ON DELETE statement' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) expect(model).not_to receive(:execute).with(/ON DELETE/) @@ -266,10 +269,11 @@ describe Gitlab::Database::MigrationHelpers do it 'creates a concurrent foreign key and validates it' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id) end @@ -293,10 +297,11 @@ describe Gitlab::Database::MigrationHelpers do it 'creates a new foreign key' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+foo/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :foo) end @@ -321,10 +326,11 @@ describe Gitlab::Database::MigrationHelpers do it 'creates a new foreign key' do expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT.+bar/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) model.add_concurrent_foreign_key(:projects, :users, column: :user_id, name: :bar) end @@ -361,6 +367,7 @@ describe Gitlab::Database::MigrationHelpers do aggregate_failures do expect(model).not_to receive(:concurrent_foreign_key_name) expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/) expect(model).to receive(:execute).ordered.with(/RESET ALL/) @@ -377,6 +384,7 @@ describe Gitlab::Database::MigrationHelpers do aggregate_failures do expect(model).to receive(:concurrent_foreign_key_name) expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/ALTER TABLE projects VALIDATE CONSTRAINT/) expect(model).to receive(:execute).ordered.with(/RESET ALL/) @@ -527,6 +535,26 @@ describe Gitlab::Database::MigrationHelpers do end end end + + # This spec runs without an enclosing transaction (:delete truncation method for db_cleaner) + context 'when the statement_timeout is already disabled', :delete do + before do + ActiveRecord::Base.connection.execute('SET statement_timeout TO 0') + end + + after do + # Use ActiveRecord::Base.connection instead of model.execute + # so that this call is not counted below + ActiveRecord::Base.connection.execute('RESET ALL') + end + + it 'yields control without disabling the timeout or resetting' do + expect(model).not_to receive(:execute).with('SET statement_timeout TO 0') + expect(model).not_to receive(:execute).with('RESET ALL') + + expect { |block| model.disable_statement_timeout(&block) }.to yield_control + end + end end describe '#true_value' do @@ -619,7 +647,7 @@ describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:update_column_in_batches) .with(:projects, :foo, 10) - expect(model).not_to receive(:change_column_null) + expect(model).not_to receive(:add_not_null_constraint) model.add_column_with_default(:projects, :foo, :integer, default: 10, @@ -630,8 +658,8 @@ describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:update_column_in_batches) .with(:projects, :foo, 10) - expect(model).to receive(:change_column_null) - .with(:projects, :foo, false) + expect(model).to receive(:add_not_null_constraint) + .with(:projects, :foo) model.add_column_with_default(:projects, :foo, :integer, default: 10) end @@ -650,16 +678,16 @@ describe Gitlab::Database::MigrationHelpers do end it 'removes the added column whenever changing a column NULL constraint fails' do - expect(model).to receive(:change_column_null) - .with(:projects, :foo, false) - .and_raise(RuntimeError) + expect(model).to receive(:add_not_null_constraint) + .with(:projects, :foo) + .and_raise(ActiveRecord::ActiveRecordError) expect(model).to receive(:remove_column) .with(:projects, :foo) expect do model.add_column_with_default(:projects, :foo, :integer, default: 10) - end.to raise_error(RuntimeError) + end.to raise_error(ActiveRecord::ActiveRecordError) end end @@ -671,7 +699,7 @@ describe Gitlab::Database::MigrationHelpers do allow(model).to receive(:transaction).and_yield allow(model).to receive(:column_for).with(:user_details, :foo).and_return(column) allow(model).to receive(:update_column_in_batches).with(:user_details, :foo, 10, batch_column_name: :user_id) - allow(model).to receive(:change_column_null).with(:user_details, :foo, false) + allow(model).to receive(:add_not_null_constraint).with(:user_details, :foo) allow(model).to receive(:change_column_default).with(:user_details, :foo, 10) expect(model).to receive(:add_column) @@ -693,7 +721,7 @@ describe Gitlab::Database::MigrationHelpers do allow(model).to receive(:transaction).and_yield allow(model).to receive(:column_for).with(:projects, :foo).and_return(column) allow(model).to receive(:update_column_in_batches).with(:projects, :foo, 10) - allow(model).to receive(:change_column_null).with(:projects, :foo, false) + allow(model).to receive(:add_not_null_constraint).with(:projects, :foo) allow(model).to receive(:change_column_default).with(:projects, :foo, 10) expect(model).to receive(:add_column) @@ -782,7 +810,7 @@ describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:update_column_in_batches) - expect(model).to receive(:change_column_null).with(:users, :new, false) + expect(model).to receive(:add_not_null_constraint).with(:users, :new) expect(model).to receive(:copy_indexes).with(:users, :old, :new) expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new) @@ -915,7 +943,7 @@ describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:update_column_in_batches) - expect(model).to receive(:change_column_null).with(:users, :old, false) + expect(model).to receive(:add_not_null_constraint).with(:users, :old) expect(model).to receive(:copy_indexes).with(:users, :new, :old) expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old) @@ -2225,6 +2253,7 @@ describe Gitlab::Database::MigrationHelpers do .and_return(false).exactly(1) expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/) @@ -2268,6 +2297,7 @@ describe Gitlab::Database::MigrationHelpers do .and_return(false).exactly(1) expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:with_lock_retries).and_call_original expect(model).to receive(:execute).with(/ADD CONSTRAINT check_name_not_null/) @@ -2309,6 +2339,7 @@ describe Gitlab::Database::MigrationHelpers do expect(model).to receive(:check_constraint_exists?).and_return(true) expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(validate_sql) expect(model).to receive(:execute).ordered.with(/RESET ALL/) @@ -2448,4 +2479,135 @@ describe Gitlab::Database::MigrationHelpers do end end end + + describe '#add_not_null_constraint' do + context 'when it is called with the default options' do + it 'calls add_check_constraint with an infered constraint name and validate: true' do + constraint_name = model.check_constraint_name(:test_table, + :name, + 'not_null') + check = "name IS NOT NULL" + + expect(model).to receive(:column_is_nullable?).and_return(true) + expect(model).to receive(:check_constraint_name).and_call_original + expect(model).to receive(:add_check_constraint) + .with(:test_table, check, constraint_name, validate: true) + + model.add_not_null_constraint(:test_table, :name) + end + end + + context 'when all parameters are provided' do + it 'calls add_check_constraint with the correct parameters' do + constraint_name = 'check_name_not_null' + check = "name IS NOT NULL" + + expect(model).to receive(:column_is_nullable?).and_return(true) + expect(model).not_to receive(:check_constraint_name) + expect(model).to receive(:add_check_constraint) + .with(:test_table, check, constraint_name, validate: false) + + model.add_not_null_constraint( + :test_table, + :name, + constraint_name: constraint_name, + validate: false + ) + end + end + + context 'when the column is defined as NOT NULL' do + it 'does not add a check constraint' do + expect(model).to receive(:column_is_nullable?).and_return(false) + expect(model).not_to receive(:check_constraint_name) + expect(model).not_to receive(:add_check_constraint) + + model.add_not_null_constraint(:test_table, :name) + end + end + end + + describe '#validate_not_null_constraint' do + context 'when constraint_name is not provided' do + it 'calls validate_check_constraint with an infered constraint name' do + constraint_name = model.check_constraint_name(:test_table, + :name, + 'not_null') + + expect(model).to receive(:check_constraint_name).and_call_original + expect(model).to receive(:validate_check_constraint) + .with(:test_table, constraint_name) + + model.validate_not_null_constraint(:test_table, :name) + end + end + + context 'when constraint_name is provided' do + it 'calls validate_check_constraint with the correct parameters' do + constraint_name = 'check_name_not_null' + + expect(model).not_to receive(:check_constraint_name) + expect(model).to receive(:validate_check_constraint) + .with(:test_table, constraint_name) + + model.validate_not_null_constraint(:test_table, :name, constraint_name: constraint_name) + end + end + end + + describe '#remove_not_null_constraint' do + context 'when constraint_name is not provided' do + it 'calls remove_check_constraint with an infered constraint name' do + constraint_name = model.check_constraint_name(:test_table, + :name, + 'not_null') + + expect(model).to receive(:check_constraint_name).and_call_original + expect(model).to receive(:remove_check_constraint) + .with(:test_table, constraint_name) + + model.remove_not_null_constraint(:test_table, :name) + end + end + + context 'when constraint_name is provided' do + it 'calls remove_check_constraint with the correct parameters' do + constraint_name = 'check_name_not_null' + + expect(model).not_to receive(:check_constraint_name) + expect(model).to receive(:remove_check_constraint) + .with(:test_table, constraint_name) + + model.remove_not_null_constraint(:test_table, :name, constraint_name: constraint_name) + end + end + end + + describe '#check_not_null_constraint_exists?' do + context 'when constraint_name is not provided' do + it 'calls check_constraint_exists? with an infered constraint name' do + constraint_name = model.check_constraint_name(:test_table, + :name, + 'not_null') + + expect(model).to receive(:check_constraint_name).and_call_original + expect(model).to receive(:check_constraint_exists?) + .with(:test_table, constraint_name) + + model.check_not_null_constraint_exists?(:test_table, :name) + end + end + + context 'when constraint_name is provided' do + it 'calls check_constraint_exists? with the correct parameters' do + constraint_name = 'check_name_not_null' + + expect(model).not_to receive(:check_constraint_name) + expect(model).to receive(:check_constraint_exists?) + .with(:test_table, constraint_name) + + model.check_not_null_constraint_exists?(:test_table, :name, constraint_name: constraint_name) + end + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index a6fc33ec011..d174fac29ce 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -719,6 +719,28 @@ describe Ci::Pipeline, :mailer do ) end end + + describe 'variable CI_KUBERNETES_ACTIVE' do + context 'when pipeline.has_kubernetes_active? is true' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(true) + end + + it "is incldued with value 'true'" do + expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true') + end + end + + context 'when pipeline.has_kubernetes_active? is false' do + before do + allow(pipeline).to receive(:has_kubernetes_active?).and_return(false) + end + + it 'is not included' do + expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE') + end + end + end end describe '#protected_ref?' do diff --git a/spec/serializers/accessibility_error_entity_spec.rb b/spec/serializers/accessibility_error_entity_spec.rb new file mode 100644 index 00000000000..e9bfabb7aa8 --- /dev/null +++ b/spec/serializers/accessibility_error_entity_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AccessibilityErrorEntity do + let(:entity) { described_class.new(accessibility_error) } + + describe '#as_json' do + subject { entity.as_json } + + context 'when accessibility contains an error' do + let(:accessibility_error) do + { + code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + type: "error", + typeCode: 1, + message: "Anchor element found with a valid href attribute, but no link content has been supplied.", + context: "", + selector: "#main-nav > div:nth-child(1) > a", + runner: "htmlcs", + runnerExtras: {} + } + end + + it 'contains correct accessibility error details', :aggregate_failures do + expect(subject[:code]).to eq("WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent") + expect(subject[:type]).to eq("error") + expect(subject[:type_code]).to eq(1) + expect(subject[:message]).to eq("Anchor element found with a valid href attribute, but no link content has been supplied.") + expect(subject[:context]).to eq("") + expect(subject[:selector]).to eq("#main-nav > div:nth-child(1) > a") + expect(subject[:runner]).to eq("htmlcs") + expect(subject[:runner_extras]).to be_empty + end + end + end +end diff --git a/spec/serializers/accessibility_reports_comparer_entity_spec.rb b/spec/serializers/accessibility_reports_comparer_entity_spec.rb new file mode 100644 index 00000000000..ed2c17de640 --- /dev/null +++ b/spec/serializers/accessibility_reports_comparer_entity_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AccessibilityReportsComparerEntity do + let(:entity) { described_class.new(comparer) } + let(:comparer) { Gitlab::Ci::Reports::AccessibilityReportsComparer.new(base_report, head_report) } + let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:url) { "https://gitlab.com" } + let(:single_error) do + [ + { + code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + type: "error", + typeCode: 1, + message: "Anchor element found with a valid href attribute, but no link content has been supplied.", + context: "", + selector: "#main-nav > div:nth-child(1) > a", + runner: "htmlcs", + runnerExtras: {} + } + ] + end + let(:different_error) do + [ + { + code: "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail", + type: "error", + typeCode: 1, + message: "This element has insufficient contrast at this conformance level.", + context: "Product", + selector: "#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a", + runner: "htmlcs", + runnerExtras: {} + } + ] + end + + describe '#as_json' do + subject { entity.as_json } + + context 'when base report has error and head has a different error' do + before do + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) + end + + it 'contains correct compared accessibility report details', :aggregate_failures do + expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED) + expect(subject[:resolved_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras) + expect(subject[:new_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras) + expect(subject[:existing_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras) + expect(subject[:summary]).to include(total: 2, resolved: 1, errored: 1) + end + end + + context 'when base report has error and head has the same error' do + before do + base_report.add_url(url, single_error) + head_report.add_url(url, single_error) + end + + it 'contains correct compared accessibility report details', :aggregate_failures do + expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED) + expect(subject[:new_errors]).to be_empty + expect(subject[:resolved_errors]).to be_empty + expect(subject[:existing_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras) + expect(subject[:summary]).to include(total: 1, resolved: 0, errored: 1) + end + end + + context 'when base report has no error and head has errors' do + before do + head_report.add_url(url, single_error) + end + + it 'contains correct compared accessibility report details', :aggregate_failures do + expect(subject[:status]).to eq(Gitlab::Ci::Reports::AccessibilityReportsComparer::STATUS_FAILED) + expect(subject[:resolved_errors]).to be_empty + expect(subject[:existing_errors]).to be_empty + expect(subject[:new_errors].first).to include(:code, :type, :type_code, :message, :context, :selector, :runner, :runner_extras) + expect(subject[:summary]).to include(total: 1, resolved: 0, errored: 1) + end + end + end +end diff --git a/spec/serializers/accessibility_reports_comparer_serializer_spec.rb b/spec/serializers/accessibility_reports_comparer_serializer_spec.rb new file mode 100644 index 00000000000..37dc760fdec --- /dev/null +++ b/spec/serializers/accessibility_reports_comparer_serializer_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AccessibilityReportsComparerSerializer do + let(:project) { double(:project) } + let(:serializer) { described_class.new(project: project).represent(comparer) } + let(:comparer) { Gitlab::Ci::Reports::AccessibilityReportsComparer.new(base_report, head_report) } + let(:base_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:head_report) { Gitlab::Ci::Reports::AccessibilityReports.new } + let(:url) { "https://gitlab.com" } + let(:single_error) do + [ + { + code: "WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent", + type: "error", + typeCode: 1, + message: "Anchor element found with a valid href attribute, but no link content has been supplied.", + context: "", + selector: "#main-nav > divnth-child(1) > a", + runner: "htmlcs", + runnerExtras: {} + } + ] + end + let(:different_error) do + [ + { + code: "WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail", + type: "error", + typeCode: 1, + message: "This element has insufficient contrast at this conformance level.", + context: "Product", + selector: "#main-nav > divnth-child(2) > ul > linth-child(1) > a", + runner: "htmlcs", + runnerExtras: {} + } + ] + end + + describe '#to_json' do + subject { serializer.as_json } + + context 'when base report has error and head has a different error' do + before do + base_report.add_url(url, single_error) + head_report.add_url(url, different_error) + end + + it 'matches the schema' do + expect(subject).to match_schema('entities/accessibility_reports_comparer') + end + end + + context 'when base report has no error and head has errors' do + before do + head_report.add_url(url, single_error) + end + + it 'matches the schema' do + expect(subject).to match_schema('entities/accessibility_reports_comparer') + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb index 8893ed5504b..72d672fd36c 100644 --- a/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/migration_helpers_shared_examples.rb @@ -13,10 +13,11 @@ end RSpec.shared_examples 'performs validation' do |validation_option| it 'performs validation' do expect(model).to receive(:disable_statement_timeout).and_call_original + expect(model).to receive(:statement_timeout_disabled?).and_return(false) expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).ordered.with(/NOT VALID/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) - expect(model).to receive(:execute).with(/RESET ALL/) + expect(model).to receive(:execute).ordered.with(/RESET ALL/) model.add_concurrent_foreign_key(*args, **options.merge(validation_option)) end