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

Automatic merge of gitlab-org/gitlab master

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