Commit 72f9f0d8 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 2f429887 1cec26c7
// NOTE: This module will be used in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52044
import { memoize } from 'lodash';
export const RECAPTCHA_API_URL_PREFIX = 'https://www.google.com/recaptcha/api.js';
/**
* The name which will be used for the reCAPTCHA script's onload callback
*/
export const RECAPTCHA_ONLOAD_CALLBACK_NAME = 'recaptchaOnloadCallback';
/**
* Adds the Google reCAPTCHA script tag to the head of the document, and
* returns a promise of the grecaptcha object
* (https://developers.google.com/recaptcha/docs/display#js_api).
*
* It is memoized, so there will only be one instance of the script tag ever
* added to the document.
*
* See the reCAPTCHA documentation for more details:
*
* https://developers.google.com/recaptcha/docs/display#explicit_render
*
*/
export const initRecaptchaScript = memoize(() => {
/**
* Appends the the reCAPTCHA script tag to the head of document
*/
const appendRecaptchaScript = () => {
const script = document.createElement('script');
script.src = `${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit`;
script.classList.add('js-recaptcha-script');
document.head.appendChild(script);
};
/**
* Returns a Promise which is fulfilled after the reCAPTCHA script is loaded
*/
return new Promise((resolve) => {
window[RECAPTCHA_ONLOAD_CALLBACK_NAME] = resolve;
appendRecaptchaScript();
});
});
/**
* Clears the cached memoization of the default manager.
*
* This is needed for determinism in tests.
*/
export const clearMemoizeCache = () => {
initRecaptchaScript.cache.clear();
};
...@@ -81,7 +81,7 @@ export default { ...@@ -81,7 +81,7 @@ export default {
v-gl-tooltip v-gl-tooltip
type="button" type="button"
:disabled="isLoading" :disabled="isLoading"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" class="dropdown-new gl-button btn btn-default js-pipeline-dropdown-manual-actions"
:title="__('Run manual or delayed jobs')" :title="__('Run manual or delayed jobs')"
data-toggle="dropdown" data-toggle="dropdown"
:aria-label="__('Run manual or delayed jobs')" :aria-label="__('Run manual or delayed jobs')"
......
...@@ -50,10 +50,10 @@ ...@@ -50,10 +50,10 @@
.table-action-buttons .table-action-buttons
.btn-group .btn-group
- if can?(current_user, :read_build, @project) - if can?(current_user, :read_build, @project)
= link_to download_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', download: '', title: _('Download artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Download artifacts') }, class: 'gl-button btn btn-build has-tooltip ml-0' do = link_to download_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', download: '', title: _('Download artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Download artifacts') }, class: 'gl-button btn btn-default btn-build has-tooltip ml-0' do
= sprite_icon('download') = sprite_icon('download')
= link_to browse_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', title: _('Browse artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Browse artifacts') }, class: 'gl-button btn btn-build has-tooltip' do = link_to browse_project_job_artifacts_path(@project, artifact.job), rel: 'nofollow', title: _('Browse artifacts'), data: { placement: 'top', container: 'body' }, ref: 'tooltip', aria: { label: _('Browse artifacts') }, class: 'gl-button btn btn-default btn-build has-tooltip' do
= sprite_icon('folder-open') = sprite_icon('folder-open')
- if can?(current_user, :destroy_artifacts, @project) - if can?(current_user, :destroy_artifacts, @project)
......
...@@ -99,11 +99,11 @@ ...@@ -99,11 +99,11 @@
%td %td
.gl-display-flex .gl-display-flex
- if can?(current_user, :read_job_artifacts, job) && job.artifacts? - if can?(current_user, :read_job_artifacts, job) && job.artifacts?
= link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build gl-button btn-icon btn-svg' do = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build btn-default gl-button btn-icon btn-svg' do
= sprite_icon('download') = sprite_icon('download')
- if can?(current_user, :update_build, job) - if can?(current_user, :update_build, job)
- if job.active? - if job.active?
= link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn gl-button btn-build' do = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn gl-button btn-build btn-default' do
= sprite_icon('close') = sprite_icon('close')
- elsif job.scheduled? - elsif job.scheduled?
.btn-group .btn-group
...@@ -125,7 +125,7 @@ ...@@ -125,7 +125,7 @@
= sprite_icon('time-out') = sprite_icon('time-out')
- elsif allow_retry - elsif allow_retry
- if job.playable? && !admin && can?(current_user, :update_build, job) - if job.playable? && !admin && can?(current_user, :update_build, job)
= link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn gl-button btn-build' do = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn gl-button btn-build btn-default' do
= custom_icon('icon_play') = custom_icon('icon_play')
- elsif job.retryable? - elsif job.retryable?
= link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build gl-button btn-icon btn-default' do
......
---
title: Allow cross-origin requests on /oauth/token
merge_request: 52641
author:
type: fixed
---
title: Add btn-default class for btn-build buttons
merge_request: 52093
author: Yogi (@yo)
type: other
...@@ -290,6 +290,14 @@ module Gitlab ...@@ -290,6 +290,14 @@ module Gitlab
methods: :any, methods: :any,
expose: headers_to_expose expose: headers_to_expose
end end
# Cross-origin requests must be enabled for the Authorization code with PKCE OAuth flow when used from a browser.
allow do
origins '*'
resource '/oauth/token',
credentials: false,
methods: [:post]
end
end end
# Use caching across all environments # Use caching across all environments
......
---
name: usage_data_i_testing_group_code_coverage_project_click_total
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51411
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299893
milestone: '13.8'
type: development
group: group::testing
default_enabled: true
...@@ -145,6 +145,13 @@ Note the following when promoting a secondary: ...@@ -145,6 +145,13 @@ Note the following when promoting a secondary:
a point-in-time recovery to the last known state. a point-in-time recovery to the last known state.
Data that was created on the primary while the secondary was paused will be lost. Data that was created on the primary while the secondary was paused will be lost.
NOTE:
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. If replication and verification output
shows that it is complete, you can add `--skip-preflight-checks` to make the
command complete promotion. This bug was fixed in GitLab 13.8 and later.
To promote the secondary node to primary along with preflight checks: To promote the secondary node to primary along with preflight checks:
```shell ```shell
......
...@@ -45,6 +45,12 @@ be found in `/var/opt/gitlab/gitlab-rails/shared/pages` if using Omnibus). ...@@ -45,6 +45,12 @@ be found in `/var/opt/gitlab/gitlab-rails/shared/pages` if using Omnibus).
## Preflight checks ## Preflight checks
NOTE:
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. This bug was fixed in GitLab 13.8 and
later.
Run this command to list out all preflight checks and automatically check if replication and verification are complete before scheduling a planned failover to ensure the process will go smoothly: Run this command to list out all preflight checks and automatically check if replication and verification are complete before scheduling a planned failover to ensure the process will go smoothly:
```shell ```shell
......
...@@ -233,12 +233,25 @@ To promote the secondary node: ...@@ -233,12 +233,25 @@ To promote the secondary node:
check if replication and verification are complete before scheduling a planned check if replication and verification are complete before scheduling a planned
failover to ensure the process will go smoothly: failover to ensure the process will go smoothly:
NOTE:
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. This bug was fixed in GitLab 13.8 and
later.
```shell ```shell
gitlab-ctl promotion-preflight-checks gitlab-ctl promotion-preflight-checks
``` ```
1. Promote the **secondary**: 1. Promote the **secondary**:
NOTE:
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. If replication and verification output
shows that it is complete, you can add `--skip-preflight-checks` to make the
command complete promotion. This bug was fixed in GitLab 13.8 and later.
```shell ```shell
gitlab-ctl promote-to-primary-node gitlab-ctl promote-to-primary-node
``` ```
......
...@@ -678,6 +678,20 @@ sudo /opt/gitlab/embedded/bin/gitlab-pg-ctl promote ...@@ -678,6 +678,20 @@ sudo /opt/gitlab/embedded/bin/gitlab-pg-ctl promote
GitLab 12.9 and later are [unaffected by this error](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5147). GitLab 12.9 and later are [unaffected by this error](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5147).
### Message: `ERROR - Replication is not up-to-date` during `gitlab-ctl promotion-preflight-checks`
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. This bug was fixed in GitLab 13.8 and
later.
### Message: `ERROR - Replication is not up-to-date` during `gitlab-ctl promote-to-primary-node`
In GitLab 13.7 and earlier, if you have a data type with zero items to sync,
this command reports `ERROR - Replication is not up-to-date` even if
replication is actually up-to-date. If replication and verification output
shows that it is complete, you can add `--skip-preflight-checks` to make the command complete promotion. This bug was fixed in GitLab 13.8 and later.
## Expired artifacts ## Expired artifacts
If you notice for some reason there are more artifacts on the Geo If you notice for some reason there are more artifacts on the Geo
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import { GlCard, GlEmptyState, GlLink, GlSkeletonLoader, GlTable } from '@gitlab/ui'; import { GlCard, GlEmptyState, GlLink, GlSkeletonLoader, GlTable } from '@gitlab/ui';
import api from '~/api';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility'; import { joinPaths } from '~/lib/utils/url_utility';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
...@@ -19,6 +21,7 @@ export default { ...@@ -19,6 +21,7 @@ export default {
SelectProjectsDropdown, SelectProjectsDropdown,
TimeAgoTooltip, TimeAgoTooltip,
}, },
mixins: [glFeatureFlagsMixin()],
apollo: { apollo: {
projects: { projects: {
query: getProjectsTestCoverage, query: getProjectsTestCoverage,
...@@ -98,6 +101,11 @@ export default { ...@@ -98,6 +101,11 @@ export default {
handleError() { handleError() {
this.hasError = true; this.hasError = true;
}, },
onProjectClick() {
if (this.glFeatures.usageDataITestingGroupCodeCoverageProjectClickTotal) {
api.trackRedisHllUserEvent(this.$options.usagePingProjectEvent);
}
},
selectAllProjects(allProjects) { selectAllProjects(allProjects) {
this.projectIds = Object.fromEntries(allProjects.map(({ id }) => [id, true])); this.projectIds = Object.fromEntries(allProjects.map(({ id }) => [id, true]));
this.allProjectsSelected = true; this.allProjectsSelected = true;
...@@ -154,6 +162,7 @@ export default { ...@@ -154,6 +162,7 @@ export default {
totalHeight: 15, totalHeight: 15,
}, },
averageCoverageFormatter: getFormatter(SUPPORTED_FORMATS.percentHundred), averageCoverageFormatter: getFormatter(SUPPORTED_FORMATS.percentHundred),
usagePingProjectEvent: 'i_testing_group_code_coverage_project_click_total',
}; };
</script> </script>
<template> <template>
...@@ -211,7 +220,12 @@ export default { ...@@ -211,7 +220,12 @@ export default {
</template> </template>
<template #cell(project)="{ item }"> <template #cell(project)="{ item }">
<gl-link target="_blank" :href="item.codeCoveragePath" :data-testid="`${item.id}-name`"> <gl-link
target="_blank"
:href="item.codeCoveragePath"
:data-testid="`${item.id}-name`"
@click.once="onProjectClick"
>
{{ item.name }} {{ item.name }}
</gl-link> </gl-link>
</template> </template>
......
import { EMPTY_BODY_MESSAGE } from './constants'; import { EMPTY_BODY_MESSAGE } from './constants';
export const bodyWithFallBack = (body) => body || EMPTY_BODY_MESSAGE; /**
* A helper function which validates the passed
* in body string.
*
* It returns an empty string if the body has explicitly
* been passed in as an empty string, a fallback
* message if the body is null / undefined, else
* it will return the original body string.
*
* @param {String} body the body message
*
* @return {String} the validated body message
*/
export const bodyWithFallBack = (body) => (body === '' ? '' : body || EMPTY_BODY_MESSAGE);
...@@ -7,6 +7,9 @@ class Groups::Analytics::RepositoryAnalyticsController < Groups::Analytics::Appl ...@@ -7,6 +7,9 @@ class Groups::Analytics::RepositoryAnalyticsController < Groups::Analytics::Appl
before_action :load_group before_action :load_group
before_action -> { check_feature_availability!(:group_repository_analytics) } before_action -> { check_feature_availability!(:group_repository_analytics) }
before_action -> { authorize_view_by_action!(:read_group_repository_analytics) } before_action -> { authorize_view_by_action!(:read_group_repository_analytics) }
before_action only: [:show] do
push_frontend_feature_flag(:usage_data_i_testing_group_code_coverage_project_click_total, @group, default_enabled: :yaml)
end
track_redis_hll_event :show, name: 'i_testing_group_code_coverage_visit_total', feature: :usage_data_i_testing_group_code_coverage_visit_total, feature_default_enabled: true track_redis_hll_event :show, name: 'i_testing_group_code_coverage_visit_total', feature: :usage_data_i_testing_group_code_coverage_visit_total, feature_default_enabled: true
def show def show
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- recreate_index_url = help_page_url('integration/elasticsearch.md') - recreate_index_url = help_page_url('integration/elasticsearch.md')
- recreate_index_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recreate_index_url } - recreate_index_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: recreate_index_url }
- recreate_index_text = _("Changes won't take place until the index is %{link_start}recreated%{link_end}.").html_safe % { link_start: recreate_index_link_start, link_end: '</a>'.html_safe } - recreate_index_text = _("Changes won't take place until the index is %{link_start}recreated%{link_end}.").html_safe % { link_start: recreate_index_link_start, link_end: '</a>'.html_safe }
- elasticsearch_available = Gitlab::Elastic::Helper.default.client.ping - elasticsearch_available = Gitlab::Elastic::Helper.default.ping?
%section.settings.expanded.as-elasticsearch.no-animate#js-elasticsearch-settings{ data: { qa_selector: 'elasticsearch_tab' } } %section.settings.expanded.as-elasticsearch.no-animate#js-elasticsearch-settings{ data: { qa_selector: 'elasticsearch_tab' } }
.settings-header .settings-header
......
...@@ -10,8 +10,19 @@ module Security ...@@ -10,8 +10,19 @@ module Security
::Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| ::Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
break unless pipeline.can_store_security_reports? break unless pipeline.can_store_security_reports?
record_onboarding_progress(pipeline)
Security::StoreScansService.execute(pipeline) Security::StoreScansService.execute(pipeline)
end end
end end
private
def record_onboarding_progress(pipeline)
# We only record SAST scans since it's a Free feature and available to all users
return unless pipeline.security_scans.sast.any?
OnboardingProgressService.new(pipeline.project.namespace).execute(action: :security_scan_enabled)
end
end end
end end
---
title: Capture metrics for group coverage project links
merge_request: 51411
author:
type: added
---
title: Show Response fields for vulnerabilties sourced from DAST
merge_request: 51948
author:
type: fixed
---
title: Handle network unreachable for ES settings check
merge_request: 52586
author:
type: fixed
...@@ -294,6 +294,13 @@ module Gitlab ...@@ -294,6 +294,13 @@ module Gitlab
end end
end end
# handles unreachable hosts and any other exceptions that may be raised
def ping?
client.ping
rescue
false
end
private private
def additional_index_options def additional_index_options
......
...@@ -6,6 +6,9 @@ import getProjectsTestCoverage from 'ee/analytics/repository_analytics/graphql/q ...@@ -6,6 +6,9 @@ import getProjectsTestCoverage from 'ee/analytics/repository_analytics/graphql/q
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
jest.mock('~/api.js');
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -23,6 +26,20 @@ describe('Test coverage table component', () => { ...@@ -23,6 +26,20 @@ describe('Test coverage table component', () => {
const findProjectCountById = (id) => wrapper.find(`[data-testid="${id}-count"`); const findProjectCountById = (id) => wrapper.find(`[data-testid="${id}-count"`);
const findProjectDateById = (id) => wrapper.find(`[data-testid="${id}-date"`); const findProjectDateById = (id) => wrapper.find(`[data-testid="${id}-date"`);
const mockQueryDataNode = {
fullPath: 'test/test',
name: 'test',
id: 1,
repository: {
rootRef: 'master',
},
codeCoverageSummary: {
averageCoverage: '1.45',
coverageCount: '1',
lastUpdatedOn: new Date().toISOString(),
},
};
const createComponent = ({ data = {}, mountFn = shallowMount } = {}) => { const createComponent = ({ data = {}, mountFn = shallowMount } = {}) => {
wrapper = mountFn(TestCoverageTable, { wrapper = mountFn(TestCoverageTable, {
localVue, localVue,
...@@ -52,6 +69,7 @@ describe('Test coverage table component', () => { ...@@ -52,6 +69,7 @@ describe('Test coverage table component', () => {
data = {}, data = {},
mountFn = shallowMount, mountFn = shallowMount,
queryData = {}, queryData = {},
glFeatures = {},
} = {}) => { } = {}) => {
localVue.use(VueApollo); localVue.use(VueApollo);
fakeApollo = createMockApollo([ fakeApollo = createMockApollo([
...@@ -72,6 +90,9 @@ describe('Test coverage table component', () => { ...@@ -72,6 +90,9 @@ describe('Test coverage table component', () => {
}; };
}, },
apolloProvider: fakeApollo, apolloProvider: fakeApollo,
provide: {
glFeatures,
},
}); });
}; };
...@@ -144,26 +165,19 @@ describe('Test coverage table component', () => { ...@@ -144,26 +165,19 @@ describe('Test coverage table component', () => {
yesterday.setDate(yesterday.getDate() - 1); yesterday.setDate(yesterday.getDate() - 1);
const allCoverageData = [ const allCoverageData = [
{ {
fullPath: '-', ...mockQueryDataNode,
id: 1,
name: 'should be last', name: 'should be last',
repository: { rootRef: 'master' },
codeCoveragePath: '#',
codeCoverageSummary: { codeCoverageSummary: {
averageCoverage: '1.45', ...mockQueryDataNode.codeCoverageSummary,
coverageCount: '1',
lastUpdatedOn: yesterday.toISOString(), lastUpdatedOn: yesterday.toISOString(),
}, },
}, },
{ {
fullPath: '-', ...mockQueryDataNode,
id: 2,
name: 'should be first', name: 'should be first',
repository: { rootRef: 'master' }, id: 2,
codeCoveragePath: '#',
codeCoverageSummary: { codeCoverageSummary: {
averageCoverage: '1.45', ...mockQueryDataNode.codeCoverageSummary,
coverageCount: '1',
lastUpdatedOn: today.toISOString(), lastUpdatedOn: today.toISOString(),
}, },
}, },
...@@ -198,17 +212,12 @@ describe('Test coverage table component', () => { ...@@ -198,17 +212,12 @@ describe('Test coverage table component', () => {
projects: { projects: {
nodes: [ nodes: [
{ {
...mockQueryDataNode,
fullPath, fullPath,
name: 'test',
id, id,
repository: { repository: {
rootRef, rootRef,
}, },
codeCoverageSummary: {
averageCoverage: '1.45',
coverageCount: '1',
lastUpdatedOn: new Date().toISOString(),
},
}, },
], ],
}, },
...@@ -222,6 +231,77 @@ describe('Test coverage table component', () => { ...@@ -222,6 +231,77 @@ describe('Test coverage table component', () => {
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).attributes('href')).toBe(expectedPath); expect(findProjectNameById(id).attributes('href')).toBe(expectedPath);
}); });
describe('with usage metrics', () => {
describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal enabled', () => {
it('tracks i_testing_group_code_coverage_project_click_total metric', async () => {
const id = 1;
createComponentWithApollo({
data: {
projectIds: { [id]: true },
},
queryData: {
data: {
projects: {
nodes: [
{
...mockQueryDataNode,
id,
},
],
},
},
},
mountFn: mount,
glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: true },
});
// We have to wait for apollo to make the mock query and fill the table before
// we can click on the project link inside the table. Neither `runOnlyPendingTimers`
// nor `waitForPromises` work on their own to accomplish this.
jest.runOnlyPendingTimers();
await waitForPromises();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
wrapper.vm.$options.usagePingProjectEvent,
);
});
});
describe('with :usageDataITestingGroupCodeCoverageProjectClickTotal disabled', () => {
it('does not track i_testing_group_code_coverage_project_click_total metric', async () => {
const id = 1;
createComponentWithApollo({
data: {
projectIds: { [id]: true },
},
queryData: {
data: {
projects: {
nodes: [
{
...mockQueryDataNode,
id,
},
],
},
},
},
mountFn: mount,
glFeatures: { usageDataITestingGroupCodeCoverageProjectClickTotal: false },
});
// We have to wait for apollo to make the mock query and fill the table before
// we can click on the project link inside the table. Neither `runOnlyPendingTimers`
// nor `waitForPromises` work on their own to accomplish this.
jest.runOnlyPendingTimers();
await waitForPromises();
findProjectNameById(id).trigger('click');
expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled();
});
});
});
}); });
describe('when selected project has no coverage', () => { describe('when selected project has no coverage', () => {
...@@ -236,12 +316,8 @@ describe('Test coverage table component', () => { ...@@ -236,12 +316,8 @@ describe('Test coverage table component', () => {
projects: { projects: {
nodes: [ nodes: [
{ {
fullPath: 'test/test', ...mockQueryDataNode,
name: 'test',
id, id,
repository: {
rootRef: 'master',
},
codeCoverageSummary: null, codeCoverageSummary: null,
}, },
], ],
......
...@@ -174,7 +174,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -174,7 +174,9 @@ describe('VulnerabilityDetails component', () => {
}); });
describe.each([ describe.each([
['', EMPTY_BODY_MESSAGE], ['', ''],
[undefined, EMPTY_BODY_MESSAGE],
[null, EMPTY_BODY_MESSAGE],
[USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE], [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
])('with request information and body set to: %s', (body, renderedBody) => { ])('with request information and body set to: %s', (body, renderedBody) => {
let vulnerability; let vulnerability;
...@@ -234,7 +236,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -234,7 +236,9 @@ describe('VulnerabilityDetails component', () => {
}); });
describe.each([ describe.each([
['', EMPTY_BODY_MESSAGE], ['', ''],
[undefined, EMPTY_BODY_MESSAGE],
[null, EMPTY_BODY_MESSAGE],
[USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE], [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
])('with response information and body set to: %s', (body, renderedBody) => { ])('with response information and body set to: %s', (body, renderedBody) => {
let vulnerability; let vulnerability;
...@@ -281,7 +285,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -281,7 +285,9 @@ describe('VulnerabilityDetails component', () => {
}); });
describe.each([ describe.each([
['', EMPTY_BODY_MESSAGE], ['', ''],
[undefined, EMPTY_BODY_MESSAGE],
[null, EMPTY_BODY_MESSAGE],
[USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE], [USER_NOT_FOUND_MESSAGE, USER_NOT_FOUND_MESSAGE],
])('with recorded response information and body set to: %s', (body, renderedBody) => { ])('with recorded response information and body set to: %s', (body, renderedBody) => {
let vulnerability; let vulnerability;
......
...@@ -242,6 +242,12 @@ describe('Vulnerability Details', () => { ...@@ -242,6 +242,12 @@ describe('Vulnerability Details', () => {
isCode: true, isCode: true,
}; };
const EXPECT_REQUEST_WITH_EMPTY_STRING = {
label: 'Sent request:',
content: 'GET http://www.gitlab.com\nName1: Value1\nName2: Value2',
isCode: true,
};
const EXPECT_RESPONSE = { const EXPECT_RESPONSE = {
label: 'Actual response:', label: 'Actual response:',
content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
...@@ -255,6 +261,12 @@ describe('Vulnerability Details', () => { ...@@ -255,6 +261,12 @@ describe('Vulnerability Details', () => {
isCode: true, isCode: true,
}; };
const EXPECT_RESPONSE_WITH_EMPTY_STRING = {
label: 'Actual response:',
content: '500 INTERNAL SERVER ERROR\nName1: Value1\nName2: Value2',
isCode: true,
};
const EXPECT_RECORDED_RESPONSE = { const EXPECT_RECORDED_RESPONSE = {
label: 'Unmodified response:', label: 'Unmodified response:',
content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]', content: '200 OK\nName1: Value1\nName2: Value2\n\n[{"user_id":1,}]',
...@@ -267,6 +279,12 @@ describe('Vulnerability Details', () => { ...@@ -267,6 +279,12 @@ describe('Vulnerability Details', () => {
isCode: true, isCode: true,
}; };
const EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING = {
label: 'Unmodified response:',
content: '200 OK\nName1: Value1\nName2: Value2',
isCode: true,
};
const getTextContent = (el) => el.textContent.trim(); const getTextContent = (el) => el.textContent.trim();
const getLabel = (el) => getTextContent(getByTestId(el, 'label')); const getLabel = (el) => getTextContent(getByTestId(el, 'label'));
const getContent = (el) => getTextContent(getByTestId(el, 'value')); const getContent = (el) => getTextContent(getByTestId(el, 'value'));
...@@ -293,7 +311,9 @@ describe('Vulnerability Details', () => { ...@@ -293,7 +311,9 @@ describe('Vulnerability Details', () => {
${{ method: 'GET', url: 'http://www.gitlab.com' }} | ${null} ${{ method: 'GET', url: 'http://www.gitlab.com' }} | ${null}
${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${null} ${{ method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${null}
${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '[{"user_id":1,}]' }} | ${[EXPECT_REQUEST]}
${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '' }} | ${[EXPECT_REQUEST_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: null }} | ${[EXPECT_REQUEST_WITHOUT_BODY]}
${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: undefined }} | ${[EXPECT_REQUEST_WITHOUT_BODY]}
${{ headers: TEST_HEADERS, method: 'GET', url: 'http://www.gitlab.com', body: '' }} | ${[EXPECT_REQUEST_WITH_EMPTY_STRING]}
`('shows request data for $request', ({ request, expectedData }) => { `('shows request data for $request', ({ request, expectedData }) => {
createWrapper({ request }); createWrapper({ request });
expect(getSectionData('request')).toEqual(expectedData); expect(getSectionData('request')).toEqual(expectedData);
...@@ -307,7 +327,9 @@ describe('Vulnerability Details', () => { ...@@ -307,7 +327,9 @@ describe('Vulnerability Details', () => {
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }} | ${null} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]' }} | ${null}
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500' }} | ${null} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500' }} | ${null}
${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]} ${{ headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE]}
${{ headers: TEST_HEADERS, body: '', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITHOUT_BODY]} ${{ headers: TEST_HEADERS, body: null, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
${{ headers: TEST_HEADERS, body: undefined, statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITHOUT_BODY]}
${{ headers: TEST_HEADERS, body: '', statusCode: '500', reasonPhrase: 'INTERNAL SERVER ERROR' }} | ${[EXPECT_RESPONSE_WITH_EMPTY_STRING]}
`('shows response data for $response', ({ response, expectedData }) => { `('shows response data for $response', ({ response, expectedData }) => {
createWrapper({ response }); createWrapper({ response });
expect(getSectionData('response')).toEqual(expectedData); expect(getSectionData('response')).toEqual(expectedData);
...@@ -324,7 +346,9 @@ describe('Vulnerability Details', () => { ...@@ -324,7 +346,9 @@ describe('Vulnerability Details', () => {
${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200' } }]} | ${null}
${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]} | ${null} ${[{}, { response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', status_code: '200', reason_phrase: 'OK' } }]} | ${null}
${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '[{"user_id":1,}]', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE]}
${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]} ${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: null, statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: undefined, statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITHOUT_BODY]}
${[{}, { name: SUPPORTING_MESSAGE_TYPES.RECORDED, response: { headers: TEST_HEADERS, body: '', statusCode: '200', reasonPhrase: 'OK' } }]} | ${[EXPECT_RECORDED_RESPONSE_WITH_EMPTY_STRING]}
`('shows response data for $supporting_messages', ({ supportingMessages, expectedData }) => { `('shows response data for $supporting_messages', ({ supportingMessages, expectedData }) => {
createWrapper({ supportingMessages }); createWrapper({ supportingMessages });
expect(getSectionData('recorded-response')).toEqual(expectedData); expect(getSectionData('recorded-response')).toEqual(expectedData);
......
...@@ -359,4 +359,15 @@ RSpec.describe Gitlab::Elastic::Helper do ...@@ -359,4 +359,15 @@ RSpec.describe Gitlab::Elastic::Helper do
end end
end end
end end
describe '#ping?' do
subject { helper.ping? }
it 'does not raise any exception' do
allow(Gitlab::Elastic::Helper.default.client).to receive(:ping).and_raise(StandardError)
expect(subject).to be_falsey
expect { subject }.not_to raise_exception
end
end
end end
...@@ -218,5 +218,18 @@ RSpec.describe 'admin/application_settings/_elasticsearch_form' do ...@@ -218,5 +218,18 @@ RSpec.describe 'admin/application_settings/_elasticsearch_form' do
expect(rendered).to include('Retry migration') expect(rendered).to include('Retry migration')
end end
end end
context 'when elasticsearch is unreachable' do
before do
allow(Gitlab::Elastic::Helper.default).to receive(:ping?).and_return(false)
end
it 'does not show the retry migration card' do
render
expect(rendered).not_to include('There is a halted Elasticsearch migration')
expect(rendered).not_to include('Retry migration')
end
end
end end
end end
...@@ -4,11 +4,11 @@ require 'spec_helper' ...@@ -4,11 +4,11 @@ require 'spec_helper'
RSpec.describe Security::StoreScansWorker do RSpec.describe Security::StoreScansWorker do
let_it_be(:sast_scan) { create(:security_scan, scan_type: :sast) } let_it_be(:sast_scan) { create(:security_scan, scan_type: :sast) }
let_it_be(:sast_pipeline) { sast_scan.pipeline } let_it_be(:pipeline) { sast_scan.pipeline }
let_it_be(:sast_build) { sast_pipeline.security_scans.sast.last&.build } let_it_be(:sast_build) { pipeline.security_scans.sast.last&.build }
describe '#perform' do describe '#perform' do
subject(:run_worker) { described_class.new.perform(sast_pipeline.id) } subject(:run_worker) { described_class.new.perform(pipeline.id) }
before do before do
allow(Security::StoreScansService).to receive(:execute) allow(Security::StoreScansService).to receive(:execute)
...@@ -25,6 +25,8 @@ RSpec.describe Security::StoreScansWorker do ...@@ -25,6 +25,8 @@ RSpec.describe Security::StoreScansWorker do
expect(Security::StoreScansService).not_to have_received(:execute) expect(Security::StoreScansService).not_to have_received(:execute)
end end
it_behaves_like 'does not record an onboarding progress action'
end end
context 'when security reports can be stored for the pipeline' do context 'when security reports can be stored for the pipeline' do
...@@ -35,6 +37,18 @@ RSpec.describe Security::StoreScansWorker do ...@@ -35,6 +37,18 @@ RSpec.describe Security::StoreScansWorker do
expect(Security::StoreScansService).to have_received(:execute) expect(Security::StoreScansService).to have_received(:execute)
end end
it_behaves_like 'records an onboarding progress action', :security_scan_enabled do
let(:namespace) { pipeline.project.namespace }
end
context 'dast scan' do
let_it_be(:dast_scan) { create(:security_scan, scan_type: :dast) }
let_it_be(:pipeline) { dast_scan.pipeline }
let_it_be(:dast_build) { pipeline.security_scans.dast.last&.build }
it_behaves_like 'does not record an onboarding progress action'
end
end end
end end
end end
...@@ -268,6 +268,11 @@ ...@@ -268,6 +268,11 @@
redis_slot: testing redis_slot: testing
aggregation: weekly aggregation: weekly
feature_flag: usage_data_i_testing_web_performance_widget_total feature_flag: usage_data_i_testing_web_performance_widget_total
- name: i_testing_group_code_coverage_project_click_total
category: testing
redis_slot: testing
aggregation: weekly
feature_flag: usage_data_i_testing_group_code_coverage_project_click_total
# Project Management group # Project Management group
- name: g_project_management_issue_title_changed - name: g_project_management_issue_title_changed
category: issues_edit category: issues_edit
......
import {
RECAPTCHA_API_URL_PREFIX,
RECAPTCHA_ONLOAD_CALLBACK_NAME,
clearMemoizeCache,
initRecaptchaScript,
} from '~/captcha/init_recaptcha_script';
describe('initRecaptchaScript', () => {
afterEach(() => {
// NOTE: The DOM is guaranteed to be clean at the start of a new test file, but it isn't cleaned
// between examples within a file, so we need to clean it after each one. See more context here:
// - https://github.com/facebook/jest/issues/1224
// - https://stackoverflow.com/questions/42805128/does-jest-reset-the-jsdom-document-after-every-suite-or-test
//
// Also note as mentioned in https://github.com/facebook/jest/issues/1224#issuecomment-444586798
// that properties of `window` are NOT cleared between test files. So, we are also
// explicitly unsetting it.
document.head.innerHTML = '';
window[RECAPTCHA_ONLOAD_CALLBACK_NAME] = undefined;
clearMemoizeCache();
});
const triggerScriptOnload = (...args) => window[RECAPTCHA_ONLOAD_CALLBACK_NAME](...args);
describe('when called', () => {
let result;
beforeEach(() => {
result = initRecaptchaScript();
});
it('adds script to head', () => {
expect(document.head).toMatchInlineSnapshot(`
<head>
<script
class="js-recaptcha-script"
src="${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit"
/>
</head>
`);
});
it('is memoized', () => {
expect(initRecaptchaScript()).toBe(result);
expect(document.head.querySelectorAll('script').length).toBe(1);
});
it('when onload is triggered, resolves promise', async () => {
const instance = {};
triggerScriptOnload(instance);
await expect(result).resolves.toBe(instance);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Oauth::TokensController do
it 'allows cross-origin POST requests' do
post '/oauth/token', headers: { 'Origin' => 'http://notgitlab.com' }
expect(response.headers['Access-Control-Allow-Origin']).to eq '*'
expect(response.headers['Access-Control-Allow-Methods']).to eq 'POST'
expect(response.headers['Access-Control-Allow-Headers']).to be_nil
expect(response.headers['Access-Control-Allow-Credentials']).to be_nil
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment