diff options
Diffstat (limited to 'spec')
17 files changed, 465 insertions, 149 deletions
diff --git a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb b/spec/controllers/concerns/controller_with_feature_category/config_spec.rb new file mode 100644 index 00000000000..9b8ffd2baab --- /dev/null +++ b/spec/controllers/concerns/controller_with_feature_category/config_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "fast_spec_helper" +require "rspec-parameterized" +require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config" + +RSpec.describe ControllerWithFeatureCategory::Config do + describe "#matches?" do + using RSpec::Parameterized::TableSyntax + + where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do + nil | nil | nil | nil | "action" | true + [:included] | nil | nil | nil | "action" | false + [:included] | nil | nil | nil | "included" | true + nil | [:excluded] | nil | nil | "excluded" | false + nil | nil | true | nil | "action" | true + [:included] | nil | true | nil | "action" | false + [:included] | nil | true | nil | "included" | true + nil | [:excluded] | true | nil | "excluded" | false + nil | nil | false | nil | "action" | false + [:included] | nil | false | nil | "action" | false + [:included] | nil | false | nil | "included" | false + nil | [:excluded] | false | nil | "excluded" | false + nil | nil | nil | true | "action" | false + [:included] | nil | nil | true | "action" | false + [:included] | nil | nil | true | "included" | false + nil | [:excluded] | nil | true | "excluded" | false + nil | nil | nil | false | "action" | true + [:included] | nil | nil | false | "action" | false + [:included] | nil | nil | false | "included" | true + nil | [:excluded] | nil | false | "excluded" | false + nil | nil | true | false | "action" | true + [:included] | nil | true | false | "action" | false + [:included] | nil | true | false | "included" | true + nil | [:excluded] | true | false | "excluded" | false + nil | nil | false | true | "action" | false + [:included] | nil | false | true | "action" | false + [:included] | nil | false | true | "included" | false + nil | [:excluded] | false | true | "excluded" | false + end + + with_them do + let(:config) do + if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc } + unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc } + + described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc) + end + + specify { expect(config.matches?(test_action)).to be(expected) } + end + end +end diff --git a/spec/controllers/concerns/controller_with_feature_category_spec.rb b/spec/controllers/concerns/controller_with_feature_category_spec.rb new file mode 100644 index 00000000000..e603a7d14c4 --- /dev/null +++ b/spec/controllers/concerns/controller_with_feature_category_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require_relative "../../../app/controllers/concerns/controller_with_feature_category" +require_relative "../../../app/controllers/concerns/controller_with_feature_category/config" + +RSpec.describe ControllerWithFeatureCategory do + describe ".feature_category_for_action" do + let(:base_controller) do + Class.new do + include ControllerWithFeatureCategory + end + end + + let(:controller) do + Class.new(base_controller) do + feature_category :baz + feature_category :foo, except: %w(update edit) + feature_category :bar, only: %w(index show) + feature_category :quux, only: %w(destroy) + feature_category :quuz, only: %w(destroy) + end + end + + let(:subclass) do + Class.new(controller) do + feature_category :qux, only: %w(index) + end + end + + it "is nil when nothing was defined" do + expect(base_controller.feature_category_for_action("hello")).to be_nil + end + + it "returns the expected category", :aggregate_failures do + expect(controller.feature_category_for_action("update")).to eq(:baz) + expect(controller.feature_category_for_action("hello")).to eq(:foo) + expect(controller.feature_category_for_action("index")).to eq(:bar) + end + + it "returns the closest match for categories defined in subclasses" do + expect(subclass.feature_category_for_action("index")).to eq(:qux) + expect(subclass.feature_category_for_action("show")).to eq(:bar) + end + + it "returns the last defined feature category when multiple match" do + expect(controller.feature_category_for_action("destroy")).to eq(:quuz) + end + + it "raises an error when using including and excluding the same action" do + expect do + Class.new(base_controller) do + feature_category :hello, only: [:world], except: [:world] + end + end.to raise_error(%r(cannot configure both `only` and `except`)) + end + + it "raises an error when using unknown arguments" do + expect do + Class.new(base_controller) do + feature_category :hello, hello: :world + end + end.to raise_error(%r(unknown arguments)) + end + end +end diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb new file mode 100644 index 00000000000..4785ee9ed8f --- /dev/null +++ b/spec/controllers/every_controller_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Every controller" do + context "feature categories" do + let_it_be(:feature_categories) do + YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set + end + + let_it_be(:controller_actions) do + # This will return tuples of all controller actions defined in the routes + # Only for controllers inheriting ApplicationController + # Excluding controllers from gems (OAuth, Sidekiq) + Rails.application.routes.routes + .map { |route| route.required_defaults.presence } + .compact + .select { |route| route[:controller].present? && route[:action].present? } + .map { |route| [constantize_controller(route[:controller]), route[:action]] } + .reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) } + end + + let_it_be(:routes_without_category) do + controller_actions.map do |controller, action| + "#{controller}##{action}" unless controller.feature_category_for_action(action) + end.compact + end + + it "has feature categories" do + pending("We'll work on defining categories for all controllers: "\ + "https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463") + + expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category" + end + + it "completed controllers don't get new routes without categories" do + completed_controllers = [Projects::MergeRequestsController].map(&:to_s) + + newly_introduced_missing_category = routes_without_category.select do |route| + completed_controllers.any? { |controller| route.start_with?(controller) } + end + + expect(newly_introduced_missing_category).to be_empty + end + + it "recognizes the feature categories" do + routes_unknown_category = controller_actions.map do |controller, action| + used_category = controller.feature_category_for_action(action) + next unless used_category + next if used_category == :not_owned + + ["#{controller}##{action}", used_category] unless feature_categories.include?(used_category) + end.compact + + expect(routes_unknown_category).to be_empty, "#{routes_unknown_category.first(10)} had an unknown category" + end + + it "doesn't define or exclude categories on removed actions", :aggregate_failures do + controller_actions.group_by(&:first).each do |controller, controller_action| + existing_actions = controller_action.map(&:last) + used_actions = actions_defined_in_feature_category_config(controller) + non_existing_used_actions = used_actions - existing_actions + + expect(non_existing_used_actions).to be_empty, + "#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist" + end + end + end + + def constantize_controller(name) + "#{name.camelize}Controller".constantize + rescue NameError + nil # some controllers, like the omniauth ones are dynamic + end + + def actions_defined_in_feature_category_config(controller) + feature_category_configs = controller.send(:class_attributes)[:feature_category_config] + feature_category_configs.map do |config| + Array(config.send(:only)) + Array(config.send(:except)) + end.flatten.uniq.map(&:to_s) + end +end diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index ae994252657..ae95873cb1c 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -207,6 +207,15 @@ describe('AlertManagementList', () => { expect(findStatusDropdown().exists()).toBe(true); }); + it('does not display a dropdown status header', () => { + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alerts: { list: mockAlerts }, alertsCount, errored: false }, + loading: false, + }); + expect(findStatusDropdown().contains('.dropdown-title')).toBe(false); + }); + it('shows correct severity icons', () => { mountComponent({ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, diff --git a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/alert_sidebar_status_spec.js deleted file mode 100644 index 1c651bbacdc..00000000000 --- a/spec/frontend/alert_management/components/alert_sidebar_status_spec.js +++ /dev/null @@ -1,107 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; -import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; -import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; -import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql'; -import Tracking from '~/tracking'; -import mockAlerts from '../mocks/alerts.json'; - -const mockAlert = mockAlerts[0]; - -describe('Alert Details Sidebar Status', () => { - let wrapper; - const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); - const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); - - function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { - wrapper = mount(AlertSidebarStatus, { - propsData: { - alert: { ...mockAlert }, - ...data, - sidebarCollapsed, - projectPath: 'projectPath', - }, - mocks: { - $apollo: { - mutate: jest.fn(), - queries: { - alert: { - loading, - }, - }, - }, - }, - stubs, - }); - } - - afterEach(() => { - if (wrapper) { - wrapper.destroy(); - } - }); - - describe('updating the alert status', () => { - const mockUpdatedMutationResult = { - data: { - updateAlertStatus: { - errors: [], - alert: { - status: 'acknowledged', - }, - }, - }, - }; - - beforeEach(() => { - mountComponent({ - data: { alert: mockAlert }, - sidebarCollapsed: false, - loading: false, - }); - }); - - it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); - findStatusDropdownItem().vm.$emit('click'); - - expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ - mutation: updateAlertStatus, - variables: { - iid: '1527542', - status: 'TRIGGERED', - projectPath: 'projectPath', - }, - }); - }); - - it('stops updating when the request fails', () => { - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); - findStatusDropdownItem().vm.$emit('click'); - expect(findStatusLoadingIcon().exists()).toBe(false); - expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); - }); - }); - - describe('Snowplow tracking', () => { - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - mountComponent({ - props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, - data: { alert: mockAlert }, - loading: false, - }); - }); - - it('should track alert status updates', () => { - Tracking.event.mockClear(); - jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); - findStatusDropdownItem().vm.$emit('click'); - const status = findStatusDropdownItem().text(); - setImmediate(() => { - const { category, action, label } = trackAlertStatusUpdateOptions; - expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status }); - }); - }); - }); -}); diff --git a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js index 7969f553b29..db086782424 100644 --- a/spec/frontend/alert_management/components/alert_managment_sidebar_assignees_spec.js +++ b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js @@ -5,7 +5,7 @@ import { GlDropdownItem } from '@gitlab/ui'; import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue'; import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql'; -import mockAlerts from '../mocks/alerts.json'; +import mockAlerts from '../../mocks/alerts.json'; const mockAlert = mockAlerts[0]; diff --git a/spec/frontend/alert_management/components/alert_sidebar_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js index 2536e0c230a..5235ae63fee 100644 --- a/spec/frontend/alert_management/components/alert_sidebar_spec.js +++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js @@ -3,7 +3,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import AlertSidebar from '~/alert_management/components/alert_sidebar.vue'; import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue'; -import mockAlerts from '../mocks/alerts.json'; +import mockAlerts from '../../mocks/alerts.json'; const mockAlert = mockAlerts[0]; diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js new file mode 100644 index 00000000000..c2eaf540e9c --- /dev/null +++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js @@ -0,0 +1,129 @@ +import { mount } from '@vue/test-utils'; +import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui'; +import { trackAlertStatusUpdateOptions } from '~/alert_management/constants'; +import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue'; +import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql'; +import Tracking from '~/tracking'; +import mockAlerts from '../../mocks/alerts.json'; + +const mockAlert = mockAlerts[0]; + +describe('Alert Details Sidebar Status', () => { + let wrapper; + const findStatusDropdown = () => wrapper.find(GlDropdown); + const findStatusDropdownItem = () => wrapper.find(GlDropdownItem); + const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon); + + function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) { + wrapper = mount(AlertSidebarStatus, { + propsData: { + alert: { ...mockAlert }, + ...data, + sidebarCollapsed, + projectPath: 'projectPath', + }, + mocks: { + $apollo: { + mutate: jest.fn(), + queries: { + alert: { + loading, + }, + }, + }, + }, + stubs, + }); + } + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + } + }); + + describe('Alert Sidebar Dropdown Status', () => { + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('displays status dropdown', () => { + expect(findStatusDropdown().exists()).toBe(true); + }); + + it('displays the dropdown status header', () => { + expect(findStatusDropdown().contains('.dropdown-title')).toBe(true); + }); + + describe('updating the alert status', () => { + const mockUpdatedMutationResult = { + data: { + updateAlertStatus: { + errors: [], + alert: { + status: 'acknowledged', + }, + }, + }, + }; + + beforeEach(() => { + mountComponent({ + data: { alert: mockAlert }, + sidebarCollapsed: false, + loading: false, + }); + }); + + it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult); + findStatusDropdownItem().vm.$emit('click'); + + expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({ + mutation: updateAlertStatus, + variables: { + iid: '1527542', + status: 'TRIGGERED', + projectPath: 'projectPath', + }, + }); + }); + + it('stops updating when the request fails', () => { + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error())); + findStatusDropdownItem().vm.$emit('click'); + expect(findStatusLoadingIcon().exists()).toBe(false); + expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered'); + }); + }); + + describe('Snowplow tracking', () => { + beforeEach(() => { + jest.spyOn(Tracking, 'event'); + mountComponent({ + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, + data: { alert: mockAlert }, + loading: false, + }); + }); + + it('should track alert status updates', () => { + Tracking.event.mockClear(); + jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({}); + findStatusDropdownItem().vm.$emit('click'); + const status = findStatusDropdownItem().text(); + setImmediate(() => { + const { category, action, label } = trackAlertStatusUpdateOptions; + expect(Tracking.event).toHaveBeenCalledWith(category, action, { + label, + property: status, + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/alert_management/components/alert_management_system_note_spec.js b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js index 87dc36cc7cb..652cbc4b838 100644 --- a/spec/frontend/alert_management/components/alert_management_system_note_spec.js +++ b/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js @@ -1,6 +1,6 @@ import { shallowMount } from '@vue/test-utils'; import SystemNote from '~/alert_management/components/system_notes/system_note.vue'; -import mockAlerts from '../mocks/alerts.json'; +import mockAlerts from '../../mocks/alerts.json'; const mockAlert = mockAlerts[1]; diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index 2c7fce714f0..16aa8379f56 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -4,7 +4,6 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; import Icon from '~/vue_shared/components/icon.vue'; const DEFAULT_PROPS = { - loaded: true, user: { username: 'root', name: 'Administrator', @@ -12,6 +11,7 @@ const DEFAULT_PROPS = { bio: null, workInformation: null, status: null, + loaded: true, }, }; @@ -46,28 +46,21 @@ describe('User Popover Component', () => { }); }; - describe('Empty', () => { - beforeEach(() => { - createWrapper( - {}, - { - propsData: { - target: findTarget(), - user: { - name: null, - username: null, - location: null, - bio: null, - workInformation: null, - status: null, - }, - }, + describe('when user is loading', () => { + it('displays skeleton loaders', () => { + createWrapper({ + user: { + name: null, + username: null, + location: null, + bio: null, + workInformation: null, + status: null, + loaded: false, }, - ); - }); + }); - it('should return skeleton loaders', () => { - expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true); + expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(4); }); }); diff --git a/spec/lib/gitlab/class_attributes_spec.rb b/spec/lib/gitlab/class_attributes_spec.rb new file mode 100644 index 00000000000..f8766f20495 --- /dev/null +++ b/spec/lib/gitlab/class_attributes_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true +require 'fast_spec_helper' + +RSpec.describe Gitlab::ClassAttributes do + let(:klass) do + Class.new do + include Gitlab::ClassAttributes + + def self.get_attribute(name) + get_class_attribute(name) + end + + def self.set_attribute(name, value) + class_attributes[name] = value + end + end + end + + let(:subclass) { Class.new(klass) } + + describe ".get_class_attribute" do + it "returns values set on the class" do + klass.set_attribute(:foo, :bar) + + expect(klass.get_attribute(:foo)).to eq(:bar) + end + + it "returns values set on a superclass" do + klass.set_attribute(:foo, :bar) + + expect(subclass.get_attribute(:foo)).to eq(:bar) + end + + it "returns values from the subclass over attributes from a superclass" do + klass.set_attribute(:foo, :baz) + subclass.set_attribute(:foo, :bar) + + expect(subclass.get_attribute(:foo)).to eq(:bar) + end + end +end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index 9d6e80d12af..3508d02838c 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -167,13 +167,13 @@ RSpec.describe Gitlab::Danger::Helper do describe '#categories_for_file' do where(:path, :expected_categories) do - 'doc/foo' | [:none] - 'CONTRIBUTING.md' | [:none] - 'LICENSE' | [:none] - 'MAINTENANCE.md' | [:none] - 'PHILOSOPHY.md' | [:none] - 'PROCESS.md' | [:none] - 'README.md' | [:none] + 'doc/foo' | [:docs] + 'CONTRIBUTING.md' | [:docs] + 'LICENSE' | [:docs] + 'MAINTENANCE.md' | [:docs] + 'PHILOSOPHY.md' | [:docs] + 'PROCESS.md' | [:docs] + 'README.md' | [:docs] 'ee/doc/foo' | [:unknown] 'ee/README' | [:unknown] diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb index e4306806e0e..640bbebf0da 100644 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb @@ -9,7 +9,16 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do describe '#label' do it 'returns labels based on class name' do - expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform') + expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform', feature_category: '') + end + + it 'contains only the labels defined for metrics' do + expect(subject.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys) + end + + it 'includes the feature category if there is one' do + expect(test_worker_class).to receive(:get_feature_category).and_return('source_code_management') + expect(subject.labels).to include(feature_category: 'source_code_management') end end end diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 6ceba186660..12e98089066 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -70,6 +70,9 @@ RSpec.describe Gitlab::Metrics::WebTransaction do end describe '#labels' do + let(:request) { double(:request, format: double(:format, ref: :html)) } + let(:controller_class) { double(:controller_class, name: 'TestController') } + context 'when request goes to Grape endpoint' do before do route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)') @@ -77,8 +80,13 @@ RSpec.describe Gitlab::Metrics::WebTransaction do env['api.endpoint'] = endpoint end + it 'provides labels with the method and path of the route in the grape endpoint' do - expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' }) + expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: '' }) + end + + it 'contains only the labels defined for transactions' do + expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys) end it 'does not provide labels if route infos are missing' do @@ -92,24 +100,25 @@ RSpec.describe Gitlab::Metrics::WebTransaction do end context 'when request goes to ActionController' do - let(:request) { double(:request, format: double(:format, ref: :html)) } - before do - klass = double(:klass, name: 'TestController') - controller = double(:controller, class: klass, action_name: 'show', request: request) + controller = double(:controller, class: controller_class, action_name: 'show', request: request) env['action_controller.instance'] = controller end it 'tags a transaction with the name and action of a controller' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' }) + end + + it 'contains only the labels defined for transactions' do + expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys) end context 'when the request content type is not :html' do let(:request) { double(:request, format: double(:format, ref: :json)) } it 'appends the mime type to the transaction action' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: '' }) end end @@ -117,7 +126,14 @@ RSpec.describe Gitlab::Metrics::WebTransaction do let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) } it 'does not append the MIME type to the transaction action' do - expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' }) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' }) + end + end + + context 'when the feature category is known' do + it 'includes it in the feature category label' do + expect(controller_class).to receive(:feature_category_for_action).with('show').and_return(:source_code_management) + expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: "source_code_management" }) end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index be761060b1b..cad99beaf20 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -93,6 +93,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do end end + context 'for monitor' do + it 'includes accurate usage_activity_by_stage data' do + for_defined_days_back do + user = create(:user, dashboard: 'operations') + cluster = create(:cluster, user: user) + create(:project, creator: user) + create(:clusters_applications_prometheus, :installed, cluster: cluster) + end + + expect(described_class.uncached_data[:usage_activity_by_stage][:monitor]).to include( + clusters: 2, + clusters_applications_prometheus: 2, + operations_dashboard_default_dashboard: 2 + ) + expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:monitor]).to include( + clusters: 1, + clusters_applications_prometheus: 1, + operations_dashboard_default_dashboard: 1 + ) + end + end + it 'ensures recorded_at is set before any other usage data calculation' do %i(alt_usage_data redis_usage_data distinct_count count).each do |method| expect(described_class).not_to receive(method) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 4853fc2438e..2852a86eeaf 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -12,6 +12,8 @@ RSpec.describe MergeRequest do subject { create(:merge_request) } describe 'associations' do + subject { build_stubbed(:merge_request) } + it { is_expected.to belong_to(:target_project).class_name('Project') } it { is_expected.to belong_to(:source_project).class_name('Project') } it { is_expected.to belong_to(:merge_user).class_name("User") } diff --git a/spec/presenters/alert_management/alert_presenter_spec.rb b/spec/presenters/alert_management/alert_presenter_spec.rb index 9a92048c487..b1bf7029f3e 100644 --- a/spec/presenters/alert_management/alert_presenter_spec.rb +++ b/spec/presenters/alert_management/alert_presenter_spec.rb @@ -12,7 +12,7 @@ RSpec.describe AlertManagement::AlertPresenter do } end let_it_be(:alert) do - create(:alert_management_alert, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload) + create(:alert_management_alert, :with_description, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload) end subject(:presenter) { described_class.new(alert) } @@ -29,7 +29,8 @@ RSpec.describe AlertManagement::AlertPresenter do **Severity:** #{presenter.severity}#{markdown_line_break} **Service:** #{alert.service}#{markdown_line_break} **Monitoring tool:** #{alert.monitoring_tool}#{markdown_line_break} - **Hosts:** #{alert.hosts.join(' ')} + **Hosts:** #{alert.hosts.join(' ')}#{markdown_line_break} + **Description:** #{alert.description} #### Alert Details |