Commit c35a9bab authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 9ce5b1c2 27b43a19
e342c59d0c6575245a335bbe9dfe95d9a06b3a2f 9ed0124f6cdfc359521feae325420549781d883e
# frozen_string_literal: true
class Groups::EmailCampaignsController < Groups::ApplicationController
include InProductMarketingHelper
include Gitlab::Tracking::ControllerConcern
EMAIL_CAMPAIGNS_SCHEMA_URL = 'iglu:com.gitlab/email_campaigns/jsonschema/1-0-0'
feature_category :navigation
before_action :check_params
def index
track_click
redirect_to redirect_link
end
private
def track_click
data = {
namespace_id: group.id,
track: @track,
series: @series,
subject_line: subject_line(@track, @series)
}
track_self_describing_event(EMAIL_CAMPAIGNS_SCHEMA_URL, data: data)
end
def redirect_link
case @track
when :create
create_track_url
when :verify
project_pipelines_url(group.projects.first)
when :trial
'https://about.gitlab.com/free-trial/'
when :team
group_group_members_url(group)
end
end
def create_track_url
[
new_project_url,
new_project_url(anchor: 'import_project'),
help_page_url('user/project/repository/repository_mirroring')
][@series]
end
def check_params
@track = params[:track]&.to_sym
@series = params[:series]&.to_i
track_valid = @track.in?(Namespaces::InProductMarketingEmailsService::TRACKS.keys)
series_valid = @series.in?(0..Namespaces::InProductMarketingEmailsService::INTERVAL_DAYS.size - 1)
render_404 unless track_valid && series_valid
end
end
...@@ -187,9 +187,9 @@ module InProductMarketingHelper ...@@ -187,9 +187,9 @@ module InProductMarketingHelper
def cta_link(track, series, group, format: nil) def cta_link(track, series, group, format: nil)
case format case format
when :html when :html
link_to in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group), target: '_blank', rel: 'noopener noreferrer' link_to in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series), target: '_blank', rel: 'noopener noreferrer'
else else
[in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group)].join(' >> ') [in_product_marketing_cta_text(track, series), group_email_campaigns_url(group, track: track, series: series)].join(' >> ')
end end
end end
...@@ -260,31 +260,6 @@ module InProductMarketingHelper ...@@ -260,31 +260,6 @@ module InProductMarketingHelper
}[track][series] }[track][series]
end end
def in_product_marketing_cta_link(track, series, group)
{
create: [
new_project_url,
new_project_url(anchor: 'import_project'),
help_page_url('user/project/repository/repository_mirroring')
],
verify: [
project_pipelines_url(group.projects.first),
project_pipelines_url(group.projects.first),
project_pipelines_url(group.projects.first)
],
trial: [
'https://about.gitlab.com/free-trial/',
'https://about.gitlab.com/free-trial/',
'https://about.gitlab.com/free-trial/'
],
team: [
group_group_members_url(group),
group_group_members_url(group),
group_group_members_url(group)
]
}[track][series]
end
def project_link(format) def project_link(format)
link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format) link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format)
end end
......
---
title: Add the Manage::Import total GMAU metric
merge_request: 51496
author:
type: added
...@@ -111,6 +111,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do ...@@ -111,6 +111,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
resources :container_registries, only: [:index, :show], controller: 'registry/repositories' resources :container_registries, only: [:index, :show], controller: 'registry/repositories'
resource :dependency_proxy, only: [:show, :update] resource :dependency_proxy, only: [:show, :update]
resources :email_campaigns, only: :index
end end
scope(path: '*id', scope(path: '*id',
......
...@@ -66,10 +66,12 @@ module EE ...@@ -66,10 +66,12 @@ module EE
end end
def vulnerability_issue_build_parameters def vulnerability_issue_build_parameters
issue = params[:issue]
{ {
title: _("Investigate vulnerability: %{title}") % { title: vulnerability.title }, title: issue.fetch(:title, _("Investigate vulnerability: %{title}") % { title: vulnerability.title }),
description: render_vulnerability_description, description: issue.fetch(:description, render_vulnerability_description),
confidential: true confidential: issue.fetch(:confidential, true)
} }
end end
......
---
title: Fixed bug that overwrote issue description changes from vulnerabilities
merge_request: 52376
author:
type: fixed
--- ---
name: repository_push_audit_event name: repository_push_audit_event
introduced_by_url: introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/15667
rollout_issue_url: rollout_issue_url:
milestone: milestone: '12.3'
type: development type: development
group: group: group::compliance
default_enabled: false default_enabled: false
...@@ -112,6 +112,15 @@ RSpec.describe Projects::IssuesController do ...@@ -112,6 +112,15 @@ RSpec.describe Projects::IssuesController do
expect(project.issues.last.vulnerability_links.first.vulnerability).to eq(vulnerability) expect(project.issues.last.vulnerability_links.first.vulnerability).to eq(vulnerability)
end end
it 'overwrites the default fields' do
send_request
issue = project.issues.last
expect(issue.title).to eq('Title')
expect(issue.description).to eq('Description')
expect(issue.confidential).to be false
end
context 'when vulnerability already has a linked issue' do context 'when vulnerability already has a linked issue' do
render_views render_views
...@@ -131,7 +140,7 @@ RSpec.describe Projects::IssuesController do ...@@ -131,7 +140,7 @@ RSpec.describe Projects::IssuesController do
post :create, params: { post :create, params: {
namespace_id: project.namespace.to_param, namespace_id: project.namespace.to_param,
project_id: project, project_id: project,
issue: { title: 'Title', description: 'Description' }, issue: { title: 'Title', description: 'Description', confidential: 'false' },
vulnerability_id: vulnerability.id vulnerability_id: vulnerability.id
} }
end end
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import AnalyzerConfiguration from 'ee/security_configuration/sast/components/analyzer_configuration.vue'; import AnalyzerConfiguration from 'ee/security_configuration/sast/components/analyzer_configuration.vue';
import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue'; import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue';
import { makeAnalyzerEntities, makeEntities, makeSastCiConfiguration } from './helpers'; import { makeAnalyzerEntities, makeEntities, makeSastCiConfiguration } from '../helpers';
describe('AnalyzerConfiguration component', () => { describe('AnalyzerConfiguration component', () => {
let wrapper; let wrapper;
......
import { merge } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SASTConfigurationApp from 'ee/security_configuration/sast/components/app.vue'; import SASTConfigurationApp from 'ee/security_configuration/sast/components/app.vue';
import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue'; import ConfigurationForm from 'ee/security_configuration/sast/components/configuration_form.vue';
import { makeSastCiConfiguration } from './helpers'; import { stripTypenames } from 'helpers/graphql_helpers';
import createMockApollo from 'helpers/mock_apollo_helper';
import sastCiConfigurationQuery from 'ee/security_configuration/sast/graphql/sast_ci_configuration.query.graphql';
import { sastCiConfigurationQueryResponse } from '../mock_data';
Vue.use(VueApollo);
const sastDocumentationPath = '/help/sast'; const sastDocumentationPath = '/help/sast';
const projectPath = 'namespace/project'; const projectPath = 'namespace/project';
...@@ -10,29 +18,29 @@ const projectPath = 'namespace/project'; ...@@ -10,29 +18,29 @@ const projectPath = 'namespace/project';
describe('SAST Configuration App', () => { describe('SAST Configuration App', () => {
let wrapper; let wrapper;
const createComponent = ({ const pendingHandler = () => new Promise(() => {});
stubs = {}, const successHandler = async () => sastCiConfigurationQueryResponse;
loading = true, const failureHandler = async () => ({ errors: [{ message: 'some error' }] });
hasLoadingError = false, const createMockApolloProvider = (handler) =>
sastCiConfiguration = null, createMockApollo([[sastCiConfigurationQuery, handler]]);
} = {}) => {
wrapper = shallowMount(SASTConfigurationApp, { const createComponent = (options) => {
mocks: { $apollo: { loading } }, wrapper = shallowMount(
stubs, SASTConfigurationApp,
merge(
{
// Use a function reference here so it's lazily initialized, and can
// be replaced with other handlers in certain tests without
// initialising twice.
apolloProvider: () => createMockApolloProvider(successHandler),
provide: { provide: {
sastDocumentationPath, sastDocumentationPath,
projectPath, projectPath,
}, },
// While setting data is usually frowned upon, it is the documented way
// of mocking GraphQL response data:
// https://docs.gitlab.com/ee/development/fe_guide/graphql.html#testing
data() {
return {
hasLoadingError,
sastCiConfiguration,
};
}, },
}); options,
),
);
}; };
const findHeader = () => wrapper.find('header'); const findHeader = () => wrapper.find('header');
...@@ -98,7 +106,7 @@ describe('SAST Configuration App', () => { ...@@ -98,7 +106,7 @@ describe('SAST Configuration App', () => {
describe('when loading', () => { describe('when loading', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
loading: true, apolloProvider: createMockApolloProvider(pendingHandler),
}); });
}); });
...@@ -118,8 +126,7 @@ describe('SAST Configuration App', () => { ...@@ -118,8 +126,7 @@ describe('SAST Configuration App', () => {
describe('when loading failed', () => { describe('when loading failed', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
loading: false, apolloProvider: createMockApolloProvider(failureHandler),
hasLoadingError: true,
}); });
}); });
...@@ -137,14 +144,8 @@ describe('SAST Configuration App', () => { ...@@ -137,14 +144,8 @@ describe('SAST Configuration App', () => {
}); });
describe('when loaded', () => { describe('when loaded', () => {
let sastCiConfiguration;
beforeEach(() => { beforeEach(() => {
sastCiConfiguration = makeSastCiConfiguration(); createComponent();
createComponent({
loading: false,
sastCiConfiguration,
});
}); });
it('does not display a loading spinner', () => { it('does not display a loading spinner', () => {
...@@ -156,7 +157,9 @@ describe('SAST Configuration App', () => { ...@@ -156,7 +157,9 @@ describe('SAST Configuration App', () => {
}); });
it('passes the sastCiConfiguration to the sastCiConfiguration prop', () => { it('passes the sastCiConfiguration to the sastCiConfiguration prop', () => {
expect(findConfigurationForm().props('sastCiConfiguration')).toBe(sastCiConfiguration); expect(findConfigurationForm().props('sastCiConfiguration')).toEqual(
stripTypenames(sastCiConfigurationQueryResponse.data.project.sastCiConfiguration),
);
}); });
it('does not display an alert message', () => { it('does not display an alert message', () => {
......
...@@ -8,7 +8,7 @@ import ExpandableSection from 'ee/security_configuration/sast/components/expanda ...@@ -8,7 +8,7 @@ import ExpandableSection from 'ee/security_configuration/sast/components/expanda
import configureSastMutation from 'ee/security_configuration/sast/graphql/configure_sast.mutation.graphql'; import configureSastMutation from 'ee/security_configuration/sast/graphql/configure_sast.mutation.graphql';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import { makeEntities, makeSastCiConfiguration } from './helpers'; import { makeEntities, makeSastCiConfiguration } from '../helpers';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(), redirectTo: jest.fn(),
......
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue'; import DynamicFields from 'ee/security_configuration/sast/components/dynamic_fields.vue';
import { makeEntities } from './helpers'; import { makeEntities } from '../helpers';
describe('DynamicFields component', () => { describe('DynamicFields component', () => {
let wrapper; let wrapper;
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
toSastCiConfigurationEntityInput, toSastCiConfigurationEntityInput,
toSastCiConfigurationAnalyzerEntityInput, toSastCiConfigurationAnalyzerEntityInput,
} from 'ee/security_configuration/sast/components/utils'; } from 'ee/security_configuration/sast/components/utils';
import { makeEntities, makeAnalyzerEntities } from './helpers'; import { makeEntities, makeAnalyzerEntities } from '../helpers';
describe('isValidConfigurationEntity', () => { describe('isValidConfigurationEntity', () => {
const validEntities = makeEntities(3); const validEntities = makeEntities(3);
......
...@@ -15,6 +15,7 @@ export const makeEntities = (count, changes) => ...@@ -15,6 +15,7 @@ export const makeEntities = (count, changes) =>
label: `label${i}`, label: `label${i}`,
type: 'string', type: 'string',
value: `value${i}`, value: `value${i}`,
size: `MEDIUM`,
...changes, ...changes,
})); }));
......
import { makeEntities } from './helpers';
export const sastCiConfigurationQueryResponse = {
data: {
project: {
sastCiConfiguration: {
global: {
nodes: makeEntities(2, { __typename: 'SastCiConfigurationEntity' }),
__typename: 'SastCiConfigurationEntityConnection',
},
pipeline: {
nodes: makeEntities(2, { __typename: 'SastCiConfigurationEntity' }),
__typename: 'SastCiConfigurationEntityConnection',
},
analyzers: {
nodes: [
{
description: 'Ruby on Rails',
enabled: false,
label: 'Brakeman',
name: 'brakeman',
variables: {
nodes: makeEntities(2, { __typename: 'SastCiConfigurationEntity' }),
__typename: 'SastCiConfigurationEntityConnection',
},
__typename: 'SastCiConfigurationAnalyzersEntity',
},
{
description: 'Python',
enabled: false,
label: 'Bandit',
name: 'bandit',
variables: {
nodes: [],
__typename: 'SastCiConfigurationEntityConnection',
},
__typename: 'SastCiConfigurationAnalyzersEntity',
},
],
__typename: 'SastCiConfigurationAnalyzersEntityConnection',
},
__typename: 'SastCiConfiguration',
},
__typename: 'Project',
},
},
};
...@@ -582,6 +582,7 @@ module Gitlab ...@@ -582,6 +582,7 @@ module Gitlab
users_created: count(::User.where(time_period), start: user_minimum_id, finish: user_maximum_id), users_created: count(::User.where(time_period), start: user_minimum_id, finish: user_maximum_id),
omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' }, omniauth_providers: filtered_omniauth_provider_names.reject { |name| name == 'group_saml' },
user_auth_by_provider: distinct_count_user_auth_by_provider(time_period), user_auth_by_provider: distinct_count_user_auth_by_provider(time_period),
unique_users_all_imports: unique_users_all_imports(time_period),
bulk_imports: { bulk_imports: {
gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id) gitlab: distinct_count(::BulkImport.where(time_period, source_type: :gitlab), :user_id)
}, },
...@@ -903,6 +904,18 @@ module Gitlab ...@@ -903,6 +904,18 @@ module Gitlab
distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord distinct_count(::Project.imported_from(from).where(time_period).where.not(import_type: nil), :creator_id) # rubocop: disable CodeReuse/ActiveRecord
end end
# rubocop:disable CodeReuse/ActiveRecord
def unique_users_all_imports(time_period)
project_imports = distinct_count(::Project.where(time_period).where.not(import_type: nil), :creator_id)
bulk_imports = distinct_count(::BulkImport.where(time_period), :user_id)
jira_issue_imports = distinct_count(::JiraImportState.where(time_period), :user_id)
csv_issue_imports = distinct_count(Issues::CsvImport.where(time_period), :user_id)
group_imports = distinct_count(::GroupImportState.where(time_period), :user_id)
project_imports + bulk_imports + jira_issue_imports + csv_issue_imports + group_imports
end
# rubocop:enable CodeReuse/ActiveRecord
# rubocop:disable CodeReuse/ActiveRecord # rubocop:disable CodeReuse/ActiveRecord
def distinct_count_user_auth_by_provider(time_period) def distinct_count_user_auth_by_provider(time_period)
counts = auth_providers_except_ldap.each_with_object({}) do |provider, hash| counts = auth_providers_except_ldap.each_with_object({}) do |provider, hash|
......
/**
* Returns a clone of the given object with all __typename keys omitted,
* including deeply nested ones.
*
* Only works with JSON-serializable objects.
*
* @param {object} An object with __typename keys (e.g., a GraphQL response)
* @returns {object} A new object with no __typename keys
*/
export const stripTypenames = (object) => {
return JSON.parse(
JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)),
);
};
import { stripTypenames } from './graphql_helpers';
describe('stripTypenames', () => {
it.each`
input | expected
${{}} | ${{}}
${{ __typename: 'Foo' }} | ${{}}
${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }}
${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }}
${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }}
${[]} | ${[]}
${[{ __typename: 'Foo' }]} | ${[{}]}
${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]}
`('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => {
const actual = stripTypenames(input);
expect(actual).toEqual(expected);
expect(input).not.toBe(actual);
});
it('given null returns null', () => {
expect(stripTypenames(null)).toEqual(null);
});
});
import { mount, createLocalVue } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash'; import { merge } from 'lodash';
import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import Vuex from 'vuex'; import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -26,8 +27,8 @@ import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/quer ...@@ -26,8 +27,8 @@ import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/quer
jest.mock('~/flash'); jest.mock('~/flash');
const localVue = createLocalVue(); Vue.use(VueApollo);
localVue.use(Vuex); Vue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json'; const SAST_COMPARISON_PATH = '/sast.json';
const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json'; const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
...@@ -47,7 +48,6 @@ describe('Security reports app', () => { ...@@ -47,7 +48,6 @@ describe('Security reports app', () => {
SecurityReportsApp, SecurityReportsApp,
merge( merge(
{ {
localVue,
propsData: { ...props }, propsData: { ...props },
stubs: { stubs: {
HelpIcon: true, HelpIcon: true,
...@@ -64,8 +64,6 @@ describe('Security reports app', () => { ...@@ -64,8 +64,6 @@ describe('Security reports app', () => {
Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse }); Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] }); const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => { const createMockApolloProvider = (handler) => {
localVue.use(VueApollo);
const requestHandlers = [[securityReportDownloadPathsQuery, handler]]; const requestHandlers = [[securityReportDownloadPathsQuery, handler]];
return createMockApollo(requestHandlers); return createMockApollo(requestHandlers);
......
...@@ -228,6 +228,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do ...@@ -228,6 +228,29 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
) )
end end
it 'includes import gmau usage data' do
for_defined_days_back do
user = create(:user)
group = create(:group)
group.add_owner(user)
create(:project, import_type: :github, creator_id: user.id)
create(:jira_import_state, :finished, project: create(:project, creator_id: user.id))
create(:issue_csv_import, user: user)
create(:group_import_state, group: group, user: user)
create(:bulk_import, user: user)
end
expect(described_class.usage_activity_by_stage_manage({})).to include(
unique_users_all_imports: 10
)
expect(described_class.usage_activity_by_stage_manage(described_class.last_28_days_time_period)).to include(
unique_users_all_imports: 5
)
end
it 'includes imports usage data' do it 'includes imports usage data' do
for_defined_days_back do for_defined_days_back do
user = create(:user) user = create(:user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::EmailCampaignsController do
include InProductMarketingHelper
using RSpec::Parameterized::TableSyntax
describe 'GET #index', :snowplow do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let(:track) { 'create' }
let(:series) { '0' }
before do
sign_in(user)
group.add_developer(user)
allow(Gitlab::Tracking).to receive(:self_describing_event)
end
subject(:get_index) do
get group_email_campaigns_url(group, track: track, series: series)
response
end
RSpec::Matchers.define :track_event do |*args|
match do
expect(Gitlab::Tracking).to have_received(:self_describing_event).with(
described_class::EMAIL_CAMPAIGNS_SCHEMA_URL,
data: {
namespace_id: group.id,
track: track.to_sym,
series: series.to_i,
subject_line: subject_line(track.to_sym, series.to_i)
}
)
end
match_when_negated do
expect(Gitlab::Tracking).not_to have_received(:self_describing_event)
end
end
context 'track parameter' do
where(:track, :valid) do
'create' | true
'verify' | true
'trial' | true
'team' | true
'xxxx' | false
nil | false
end
with_them do
it do
if valid
is_expected.to track_event
is_expected.to have_gitlab_http_status(:redirect)
else
is_expected.not_to track_event
is_expected.to have_gitlab_http_status(:not_found)
end
end
end
end
context 'series parameter' do
where(:series, :valid) do
'0' | true
'1' | true
'2' | true
'-1' | false
'3' | false
nil | false
end
with_them do
it do
if valid
is_expected.to track_event
is_expected.to have_gitlab_http_status(:redirect)
else
is_expected.not_to track_event
is_expected.to have_gitlab_http_status(:not_found)
end
end
end
end
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