Commit 47012f5a authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 9b1b010d b434e085
<script>
/* eslint-disable vue/no-v-html */
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { GlModal } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
DeprecatedModal,
GlModal,
},
props: {
actionUrl: {
......@@ -55,21 +55,34 @@ You are about to permanently delete %{yourAccount}, and all of the issues, merge
Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
{
yourAccount: `<strong>${s__('Profiles|your account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete Account')}</strong>`,
deleteAccount: `<strong>${s__('Profiles|Delete account')}</strong>`,
},
false,
);
},
},
methods: {
primaryProps() {
return {
text: s__('Delete account'),
attributes: [{ variant: 'danger' }, { category: 'primary' }, { disabled: !this.canSubmit }],
};
},
cancelProps() {
return {
text: s__('Cancel'),
};
},
canSubmit() {
if (this.confirmWithPassword) {
return this.enteredPassword !== '';
}
return this.enteredUsername === this.username;
},
},
methods: {
onSubmit() {
if (!this.canSubmit) {
return;
}
this.$refs.form.submit();
},
},
......@@ -77,42 +90,39 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
</script>
<template>
<deprecated-modal
id="delete-account-modal"
:title="s__('Profiles|Delete your account?')"
:text="text"
:primary-button-label="s__('Profiles|Delete account')"
:submit-disabled="!canSubmit()"
kind="danger"
@submit="onSubmit"
<gl-modal
modal-id="delete-account-modal"
title="Profiles"
:action-primary="primaryProps"
:action-cancel="cancelProps"
:ok-disabled="!canSubmit"
@primary="onSubmit"
>
<template #body="props">
<p v-html="props.text"></p>
<p v-html="text"></p>
<form ref="form" :action="actionUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<form ref="form" :action="actionUrl" method="post">
<input type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<p id="input-label" v-html="inputLabel"></p>
<p id="input-label" v-html="inputLabel"></p>
<input
v-if="confirmWithPassword"
v-model="enteredPassword"
name="password"
class="form-control"
type="password"
data-qa-selector="password_confirmation_field"
aria-labelledby="input-label"
/>
<input
v-else
v-model="enteredUsername"
name="username"
class="form-control"
type="text"
aria-labelledby="input-label"
/>
</form>
</template>
</deprecated-modal>
<input
v-if="confirmWithPassword"
v-model="enteredPassword"
name="password"
class="form-control"
type="password"
data-qa-selector="password_confirmation_field"
aria-labelledby="input-label"
/>
<input
v-else
v-model="enteredUsername"
name="username"
class="form-control"
type="text"
aria-labelledby="input-label"
/>
</form>
</gl-modal>
</template>
......@@ -30,6 +30,9 @@ export default () => {
},
mounted() {
deleteAccountButton.classList.remove('disabled');
deleteAccountButton.addEventListener('click', () => {
this.$root.$emit('bv::show::modal', 'delete-account-modal', '#delete-account-button');
});
},
render(createElement) {
return createElement('delete-account-modal', {
......
......@@ -8,6 +8,7 @@ module Ci
include UpdateProjectStatistics
include Artifactable
include FileStoreMounter
include Presentable
FILE_STORE_SUPPORTED = [
ObjectStorage::Store::LOCAL,
......@@ -44,5 +45,9 @@ module Ci
def self.find_with_code_coverage
find_by(file_type: :code_coverage)
end
def present
super(presenter_class: "Ci::PipelineArtifacts::#{self.file_type.camelize}Presenter".constantize)
end
end
end
# frozen_string_literal: true
module Ci
module PipelineArtifacts
class CodeCoveragePresenter < ProcessablePresenter
include Gitlab::Utils::StrongMemoize
def for_files(filenames)
coverage_files = raw_report["files"].select { |key| filenames.include?(key) }
{ files: coverage_files }
end
private
def raw_report
strong_memoize(:raw_report) do
self.each_blob do |blob|
Gitlab::Json.parse(blob)
end
end
end
end
end
end
......@@ -12,7 +12,7 @@ module Ci
{
status: :parsed,
key: key(base_pipeline, head_pipeline),
data: Gitlab::Ci::Pipeline::Artifact::CodeCoverage.new(head_pipeline.pipeline_artifacts.find_with_code_coverage).for_files(merge_request.new_paths)
data: head_pipeline.pipeline_artifacts.find_with_code_coverage.present.for_files(merge_request.new_paths)
}
rescue => e
Gitlab::ErrorTracking.track_exception(e, project_id: project.id)
......
......@@ -55,8 +55,8 @@
= s_('Profiles|Deleting an account has the following effects:')
= render 'users/deletion_guidance', user: current_user
%button#delete-account-button.btn.btn-danger.disabled{ data: { toggle: 'modal',
target: '#delete-account-modal', qa_selector: 'delete_account_button' } }
-# Delete button here
%button#delete-account-button.btn.btn-danger.disabled{ data: { qa_selector: 'delete_account_button' } }
= s_('Profiles|Delete account')
#delete-account-modal{ data: { action_url: user_registration_path,
......
......@@ -3,13 +3,15 @@
module Vulnerabilities
module HistoricalStatistics
class DeletionService
RETENTION_PERIOD_IN_DAYS = 365
def self.execute
new.execute
end
def execute
::Vulnerabilities::HistoricalStatistic
.older_than(days: 100)
.older_than(days: RETENTION_PERIOD_IN_DAYS)
.each_batch { |relation| relation.delete_all }
end
end
......
---
title: Expand retention period to 365 days for Vulnerability Statistics
merge_request: 40833
author:
type: changed
......@@ -30,35 +30,35 @@ RSpec.describe Vulnerabilities::HistoricalStatistics::DeletionService do
create(:vulnerability_historical_statistic, project: other_project, date: 25.days.ago)
end
context 'when there is no historical statistics older than 100 days' do
context 'when there is no historical statistics older than 365 days' do
it 'does not delete historical statistics' do
expect { delete_historical_statistics }.not_to change { Vulnerabilities::HistoricalStatistic.count }
end
end
context 'when there is a historical statistic entry that was created 99 days ago' do
context 'when there is a historical statistic entry that was created 364 days ago' do
before do
create(:vulnerability_historical_statistic, project: project, date: 99.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 99.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 364.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 364.days.ago)
end
it 'does not delete historical statistics' do
expect { delete_historical_statistics }.not_to change { Vulnerabilities::HistoricalStatistic.count }
end
context 'and there are more than one entries that are older than 100 days' do
context 'and there are more than one entries that are older than 365 days' do
before do
create(:vulnerability_historical_statistic, project: project, date: 101.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 102.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 103.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 101.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 102.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 103.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 366.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 367.days.ago)
create(:vulnerability_historical_statistic, project: project, date: 368.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 366.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 367.days.ago)
create(:vulnerability_historical_statistic, project: other_project, date: 368.days.ago)
end
it 'deletes historical statistics older than 90 days', :aggregate_failures do
it 'deletes historical statistics older than 365 days', :aggregate_failures do
expect { delete_historical_statistics }.to change { Vulnerabilities::HistoricalStatistic.count }.by(-6)
expect(Vulnerabilities::HistoricalStatistic.pluck(:date)).to all(be >= 100.days.ago.to_date)
expect(Vulnerabilities::HistoricalStatistic.pluck(:date)).to all(be >= 365.days.ago.to_date)
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Pipeline
module Artifact
class CodeCoverage
include Gitlab::Utils::StrongMemoize
def initialize(pipeline_artifact)
@pipeline_artifact = pipeline_artifact
end
def for_files(filenames)
coverage_files = raw_report["files"].select { |key| filenames.include?(key) }
{ files: coverage_files }
end
private
def raw_report
strong_memoize(:raw_report) do
@pipeline_artifact.each_blob do |blob|
Gitlab::Json.parse(blob)
end
end
end
end
end
end
end
end
......@@ -7950,6 +7950,9 @@ msgstr ""
msgid "Delete Snippet"
msgstr ""
msgid "Delete account"
msgstr ""
msgid "Delete artifacts"
msgstr ""
......@@ -18727,15 +18730,9 @@ msgstr ""
msgid "Profiles|Default notification email"
msgstr ""
msgid "Profiles|Delete Account"
msgstr ""
msgid "Profiles|Delete account"
msgstr ""
msgid "Profiles|Delete your account?"
msgstr ""
msgid "Profiles|Deleting an account has the following effects:"
msgstr ""
......
......@@ -10,8 +10,12 @@ module RuboCop
MSG = 'indexes added with custom options must be explicitly named'
def_node_matcher :match_create_table_index_with_options, <<~PATTERN
(send _ {:index } _ (hash $...))
PATTERN
def_node_matcher :match_add_index_with_options, <<~PATTERN
(send _ {:add_concurrent_index} _ _ (hash $...))
(send _ {:add_index :add_concurrent_index} _ _ (hash $...))
PATTERN
def_node_matcher :name_option?, <<~PATTERN
......@@ -26,7 +30,7 @@ module RuboCop
return unless in_migration?(node)
node.each_descendant(:send) do |send_node|
next unless add_index_offense?(send_node)
next unless create_table_with_index_offense?(send_node) || add_index_offense?(send_node)
add_offense(send_node, location: :selector)
end
......@@ -34,6 +38,10 @@ module RuboCop
private
def create_table_with_index_offense?(send_node)
match_create_table_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) }
end
def add_index_offense?(send_node)
match_add_index_with_options(send_node) { |option_nodes| needs_name_option?(option_nodes) }
end
......
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import { merge } from 'lodash';
import { mount } from '@vue/test-utils';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
const GlModalStub = {
name: 'gl-modal-stub',
template: `
<div>
<slot></slot>
</div>
`,
};
describe('DeleteAccountModal component', () => {
const actionUrl = `${TEST_HOST}/delete/user`;
const username = 'hasnoname';
let Component;
let wrapper;
let vm;
beforeEach(() => {
Component = Vue.extend(deleteAccountModal);
});
const createWrapper = (options = {}) => {
wrapper = mount(
deleteAccountModal,
merge(
{},
{
propsData: {
actionUrl,
username,
},
stubs: {
GlModal: GlModalStub,
},
},
options,
),
);
vm = wrapper.vm;
};
afterEach(() => {
vm.$destroy();
wrapper.destroy();
wrapper = null;
vm = null;
});
const findElements = () => {
......@@ -23,16 +51,16 @@ describe('DeleteAccountModal component', () => {
return {
form: vm.$refs.form,
input: vm.$el.querySelector(`[name="${confirmation}"]`),
submitButton: vm.$el.querySelector('.btn-danger'),
};
};
const findModal = () => wrapper.find(GlModalStub);
describe('with password confirmation', () => {
beforeEach(done => {
vm = mountComponent(Component, {
actionUrl,
confirmWithPassword: true,
username,
createWrapper({
propsData: {
confirmWithPassword: true,
},
});
vm.isOpen = true;
......@@ -43,7 +71,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept empty password', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = '';
input.dispatchEvent(new Event('input'));
......@@ -51,8 +79,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBe('true');
findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
......@@ -61,7 +89,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with password', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'anything';
input.dispatchEvent(new Event('input'));
......@@ -69,8 +97,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredPassword).toBe(input.value);
expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBeUndefined();
findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})
......@@ -81,10 +109,10 @@ describe('DeleteAccountModal component', () => {
describe('with username confirmation', () => {
beforeEach(done => {
vm = mountComponent(Component, {
actionUrl,
confirmWithPassword: false,
username,
createWrapper({
propsData: {
confirmWithPassword: false,
},
});
vm.isOpen = true;
......@@ -95,7 +123,7 @@ describe('DeleteAccountModal component', () => {
});
it('does not accept wrong username', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = 'this is wrong';
input.dispatchEvent(new Event('input'));
......@@ -103,8 +131,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBe('true');
findModal().vm.$emit('primary');
expect(form.submit).not.toHaveBeenCalled();
})
......@@ -113,7 +141,7 @@ describe('DeleteAccountModal component', () => {
});
it('submits form with correct username', done => {
const { form, input, submitButton } = findElements();
const { form, input } = findElements();
jest.spyOn(form, 'submit').mockImplementation(() => {});
input.value = username;
input.dispatchEvent(new Event('input'));
......@@ -121,8 +149,8 @@ describe('DeleteAccountModal component', () => {
Vue.nextTick()
.then(() => {
expect(vm.enteredUsername).toBe(input.value);
expect(submitButton).not.toHaveAttr('disabled', 'disabled');
submitButton.click();
expect(findModal().attributes('ok-disabled')).toBeUndefined();
findModal().vm.$emit('primary');
expect(form.submit).toHaveBeenCalled();
})
......
......@@ -109,4 +109,14 @@ RSpec.describe Ci::PipelineArtifact, type: :model do
end
end
end
describe '#present' do
subject { coverage_report.present }
context 'when file_type is code_coverage' do
it 'uses code coverage presenter' do
expect(subject.present).to be_kind_of(Ci::PipelineArtifacts::CodeCoveragePresenter)
end
end
end
end
......@@ -2,12 +2,13 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Artifact::CodeCoverage do
RSpec.describe Ci::PipelineArtifacts::CodeCoveragePresenter do
let(:pipeline_artifact) { create(:ci_pipeline_artifact, :with_code_coverage_with_multiple_files) }
let(:code_coverage) { described_class.new(pipeline_artifact) }
subject(:presenter) { described_class.new(pipeline_artifact) }
describe '#for_files' do
subject { code_coverage.for_files(filenames) }
subject { presenter.for_files(filenames) }
context 'when code coverage has data' do
context 'when filenames is empty' do
......
......@@ -14,40 +14,120 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco
allow(cop).to receive(:in_migration?).and_return(true)
end
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
context 'when creating complex indexes as part of create_table' do
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
create_table :test_table do |t|
t.integer :column1, null: false
t.integer :column2, null: false
t.jsonb :column3
t.index :column1, unique: true
t.index :column2, where: 'column1 = 0'
^^^^^ #{described_class::MSG}
t.index :column3, using: :gin
^^^^^ #{described_class::MSG}
end
end
def down
drop_table :test_table
end
end
RUBY
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
end
end
context 'when indexes are configured with an options hash and name' do
it 'registers no offense' do
expect_no_offenses(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
create_table :test_table do |t|
t.integer :column1, null: false
t.integer :column2, null: false
t.jsonb :column3
t.index :column1, unique: true
t.index :column2, where: 'column1 = 0', name: 'my_index_1'
t.index :column3, using: :gin, name: 'my_gin_index'
end
end
def down
drop_table :test_table
end
end
RUBY
end
end
end
INDEX_NAME = 'my_test_name'
context 'when indexes are added to an existing table' do
context 'when indexes are configured with an options hash, but no name' do
it 'registers an offense' do
expect_offense(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
disable_ddl_transaction!
def up
add_concurrent_index :test_indexes, :column1
def up
add_index :test_indexes, :column1
add_concurrent_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }
^^^^^^^^^ #{described_class::MSG}
end
add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10'
def down
add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL'
^^^^^^^^^ #{described_class::MSG}
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
end
end
RUBY
def down
add_concurrent_index :test_indexes, :column4, 'unique' => true
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
end
end
add_concurrent_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL'
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
context 'when indexes are configured with an options hash and a name' do
it 'registers no offenses' do
expect_no_offenses(<<~RUBY)
class TestComplexIndexes < ActiveRecord::Migration[6.0]
DOWNTIME = false
add_concurrent_index :test_indexes, :column5, using: :gin, name: INDEX_NAME
INDEX_NAME = 'my_test_name'
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops
^^^^^^^^^^^^^^^^^^^^ #{described_class::MSG}
end
end
RUBY
disable_ddl_transaction!
def up
add_index :test_indexes, :column1
add_index :test_indexes, :column2, where: "column2 = 'value'", order: { column4: :desc }, name: 'my_index_1'
add_concurrent_index :test_indexes, :column3, where: 'column3 = 10', name: 'idx_equal_to_10'
end
def down
add_index :test_indexes, :column4, 'unique' => true, where: 'column4 IS NOT NULL', name: 'my_index_2'
expect(cop.offenses.map(&:cop_name)).to all(eq("Migration/#{described_class.name.demodulize}"))
add_concurrent_index :test_indexes, :column6, using: :gin, opclass: :gin_trgm_ops, name: INDEX_NAME
end
end
RUBY
end
end
end
end
......@@ -65,7 +145,13 @@ RSpec.describe RuboCop::Cop::Migration::ComplexIndexesRequireName, type: :ruboco
disable_ddl_transaction!
def up
add_concurrent_index :test_indexes, :column1, where: "some_column = 'value'"
create_table :test_table do |t|
t.integer :column1
t.index :column1, where: 'column2 IS NOT NULL'
end
add_index :test_indexes, :column1, where: "some_column = 'value'"
end
def down
......
......@@ -16,7 +16,8 @@ RSpec.describe Ci::GenerateCoverageReportsService do
let!(:base_pipeline) { nil }
it 'returns status and data', :aggregate_failures do
expect_next_instance_of(Gitlab::Ci::Pipeline::Artifact::CodeCoverage) do |instance|
expect_any_instance_of(Ci::PipelineArtifact) do |instance|
expect(instance).to receive(:present)
expect(instance).to receive(:for_files).with(merge_request.new_paths).and_call_original
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