diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 06:12:37 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-23 06:12:37 +0300 |
commit | f34b26bb882947bcc1126de19fa55eb8763af32e (patch) | |
tree | 7774da06e9e981fc80bf05b6269e1ecb8b6ab5a4 /spec | |
parent | 04dabf41f65cf1c25d80d92b1cc5568bfcca80ee (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
10 files changed, 493 insertions, 18 deletions
diff --git a/spec/features/profiles/user_creates_saved_reply_spec.rb b/spec/features/profiles/user_creates_saved_reply_spec.rb new file mode 100644 index 00000000000..1d851b5cea0 --- /dev/null +++ b/spec/features/profiles/user_creates_saved_reply_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Profile > Saved replies > User creates saved reply', :js, + feature_category: :user_profile do + let_it_be(:user) { create(:user) } + + before do + sign_in(user) + + visit profile_saved_replies_path + + wait_for_requests + end + + it 'shows the user a list of their saved replies' do + find('[data-testid="saved-reply-name-input"]').set('test') + find('[data-testid="saved-reply-content-input"]').set('Test content') + + click_button 'Save' + + wait_for_requests + + expect(page).to have_content('My saved replies (1)') + expect(page).to have_content('test') + expect(page).to have_content('Test content') + end +end diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb index c80ba06bca1..613e4a1b447 100644 --- a/spec/frontend/fixtures/saved_replies.rb +++ b/spec/frontend/fixtures/saved_replies.rb @@ -43,4 +43,32 @@ RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile d expect_graphql_errors_to_be_empty end end + + context 'when user creates saved reply' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'create_saved_reply.mutation.graphql' + + it "#{base_output_path}#{query_name}.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user, variables: { name: "Test", content: "Test content" }) + + expect_graphql_errors_to_be_empty + end + end + + context 'when user creates saved reply and it errors' do + base_input_path = 'saved_replies/queries/' + base_output_path = 'graphql/saved_replies/' + query_name = 'create_saved_reply.mutation.graphql' + + it "#{base_output_path}create_saved_reply_with_errors.mutation.graphql.json" do + query = get_graphql_query_as_string("#{base_input_path}#{query_name}") + + post_graphql(query, current_user: current_user, variables: { name: nil, content: nil }) + + expect(flattened_errors).not_to be_empty + end + end end diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js index 27c0ab96cfc..fc7f5c80d45 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js @@ -1,14 +1,18 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { stubComponent } from 'helpers/stub_component'; import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; +import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue'; import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import Tracking from '~/tracking'; import { + CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + DELETE_PACKAGE_VERSION_TRACKING_ACTION, DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; import { packageData } from '../../mock_data'; @@ -22,7 +26,7 @@ describe('PackageVersionsList', () => { name: 'version 1', }), packageData({ - id: `gid://gitlab/Packages::Package/112`, + id: 'gid://gitlab/Packages::Package/112', name: 'version 2', }), ]; @@ -31,8 +35,10 @@ describe('PackageVersionsList', () => { findLoader: () => wrapper.findComponent(PackagesListLoader), findRegistryList: () => wrapper.findComponent(RegistryList), findEmptySlot: () => wrapper.findComponent(EmptySlotStub), - findListRow: () => wrapper.findAllComponents(VersionRow), + findListRow: () => wrapper.findComponent(VersionRow), + findAllListRow: () => wrapper.findAllComponents(VersionRow), findDeletePackagesModal: () => wrapper.findComponent(DeleteModal), + findPackageListDeleteModal: () => wrapper.findComponent(DeletePackageModal), }; const mountComponent = (props) => { wrapper = shallowMountExtended(PackageVersionsList, { @@ -118,16 +124,16 @@ describe('PackageVersionsList', () => { }); it('displays package version rows', () => { - expect(uiElements.findListRow().exists()).toEqual(true); - expect(uiElements.findListRow()).toHaveLength(packageList.length); + expect(uiElements.findAllListRow().exists()).toEqual(true); + expect(uiElements.findAllListRow()).toHaveLength(packageList.length); }); it('binds the correct props', () => { - expect(uiElements.findListRow().at(0).props()).toMatchObject({ + expect(uiElements.findAllListRow().at(0).props()).toMatchObject({ packageEntity: expect.objectContaining(packageList[0]), }); - expect(uiElements.findListRow().at(1).props()).toMatchObject({ + expect(uiElements.findAllListRow().at(1).props()).toMatchObject({ packageEntity: expect.objectContaining(packageList[1]), }); }); @@ -159,6 +165,68 @@ describe('PackageVersionsList', () => { }); }); + describe.each` + description | finderFunction | deletePayload + ${'when the user can destroy the package'} | ${uiElements.findListRow} | ${packageList[0]} + ${'when the user can bulk destroy packages and deletes only one package'} | ${uiElements.findRegistryList} | ${[packageList[0]]} + `('$description', ({ finderFunction, deletePayload }) => { + let eventSpy; + const category = 'UI::NpmPackages'; + const { findPackageListDeleteModal } = uiElements; + + beforeEach(() => { + eventSpy = jest.spyOn(Tracking, 'event'); + mountComponent({ canDestroy: true }); + finderFunction().vm.$emit('delete', deletePayload); + }); + + it('passes itemToBeDeleted to the modal', () => { + expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(packageList[0]); + }); + + it('requesting delete tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + category, + REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + + describe('when modal confirms', () => { + beforeEach(() => { + findPackageListDeleteModal().vm.$emit('ok'); + }); + + it('emits delete when modal confirms', () => { + expect(wrapper.emitted('delete')[0][0]).toEqual([packageList[0]]); + }); + + it('tracks the right action', () => { + expect(eventSpy).toHaveBeenCalledWith( + category, + DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + + it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => { + await findPackageListDeleteModal().vm.$emit(event); + + expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull(); + }); + + it('canceling delete tracks the right action', () => { + findPackageListDeleteModal().vm.$emit('cancel'); + + expect(eventSpy).toHaveBeenCalledWith( + category, + CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, + expect.any(Object), + ); + }); + }); + describe('when the user can bulk destroy versions', () => { let eventSpy; const { findDeletePackagesModal, findRegistryList } = uiElements; diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js index 8cb51aaf738..9f3dcc18fb6 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js @@ -1,4 +1,4 @@ -import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; +import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; @@ -24,6 +24,7 @@ describe('VersionRow', () => { const findPackageName = () => wrapper.findComponent(GlTruncate); const findWarningIcon = () => wrapper.findComponent(GlIcon); const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox); + const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem); function createComponent({ packageEntity = packageVersion, selected = false } = {}) { wrapper = shallowMountExtended(VersionRow, { @@ -112,6 +113,31 @@ describe('VersionRow', () => { }); }); + describe('delete button', () => { + it('does not exist when package cannot be destroyed', () => { + createComponent({ packageEntity: { ...packageVersion, canDestroy: false } }); + + expect(findDeleteDropdownItem().exists()).toBe(false); + }); + + it('exists and has the correct props', () => { + createComponent(); + + expect(findDeleteDropdownItem().exists()).toBe(true); + expect(findDeleteDropdownItem().attributes()).toMatchObject({ + variant: 'danger', + }); + }); + + it('emits the delete event when the delete button is clicked', () => { + createComponent(); + + findDeleteDropdownItem().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toHaveLength(1); + }); + }); + describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js index 19d56fe8cc6..0d40cb4fde0 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js @@ -1,5 +1,5 @@ import { GlFormCheckbox, GlSprintf, GlTruncate } from '@gitlab/ui'; -import Vue, { nextTick } from 'vue'; +import Vue from 'vue'; import VueRouter from 'vue-router'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; @@ -141,7 +141,6 @@ describe('packages_list_row', () => { findDeleteDropdown().vm.$emit('click'); - await nextTick(); expect(wrapper.emitted('delete')).toHaveLength(1); }); }); diff --git a/spec/frontend/saved_replies/components/form_spec.js b/spec/frontend/saved_replies/components/form_spec.js new file mode 100644 index 00000000000..693703ca572 --- /dev/null +++ b/spec/frontend/saved_replies/components/form_spec.js @@ -0,0 +1,116 @@ +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createdSavedReplyResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply.mutation.graphql.json'; +import createdSavedReplyErrorResponse from 'test_fixtures/graphql/saved_replies/create_saved_reply_with_errors.mutation.graphql.json'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import Form from '~/saved_replies/components/form.vue'; +import createSavedReplyMutation from '~/saved_replies/queries/create_saved_reply.mutation.graphql'; + +let wrapper; +let createSavedReplyResponseSpy; + +function createMockApolloProvider(response) { + Vue.use(VueApollo); + + createSavedReplyResponseSpy = jest.fn().mockResolvedValue(response); + + const requestHandlers = [[createSavedReplyMutation, createSavedReplyResponseSpy]]; + + return createMockApollo(requestHandlers); +} + +function createComponent(response = createdSavedReplyResponse) { + const mockApollo = createMockApolloProvider(response); + + return mount(Form, { + apolloProvider: mockApollo, + }); +} + +const findSavedReplyNameInput = () => wrapper.find('[data-testid="saved-reply-name-input"]'); +const findSavedReplyNameFormGroup = () => + wrapper.find('[data-testid="saved-reply-name-form-group"]'); +const findSavedReplyContentInput = () => wrapper.find('[data-testid="saved-reply-content-input"]'); +const findSavedReplyContentFormGroup = () => + wrapper.find('[data-testid="saved-reply-content-form-group"]'); +const findSavedReplyFrom = () => wrapper.find('[data-testid="saved-reply-form"]'); +const findAlerts = () => wrapper.findAllComponents(GlAlert); +const findSubmitBtn = () => wrapper.find('[data-testid="saved-reply-form-submit-btn"]'); + +describe('Saved replies form component', () => { + describe('create saved reply', () => { + it('calls apollo mutation', async () => { + wrapper = createComponent(); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(createSavedReplyResponseSpy).toHaveBeenCalledWith({ + content: 'Test content', + name: 'Test', + }); + }); + + it('does not submit when form validation fails', async () => { + wrapper = createComponent(); + + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(createSavedReplyResponseSpy).not.toHaveBeenCalled(); + }); + + it.each` + findFormGroup | findInput | fieldName + ${findSavedReplyNameFormGroup} | ${findSavedReplyContentInput} | ${'name'} + ${findSavedReplyContentFormGroup} | ${findSavedReplyNameInput} | ${'content'} + `('shows errors for empty $fieldName input', async ({ findFormGroup, findInput }) => { + wrapper = createComponent(createdSavedReplyErrorResponse); + + findInput().setValue('Test'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + expect(findFormGroup().classes('is-invalid')).toBe(true); + }); + + it('displays errors when mutation fails', async () => { + wrapper = createComponent(createdSavedReplyErrorResponse); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await waitForPromises(); + + const { errors } = createdSavedReplyErrorResponse; + const alertMessages = findAlerts().wrappers.map((x) => x.text()); + + expect(alertMessages).toEqual(errors.map((x) => x.message)); + }); + + it('shows loading state when saving', async () => { + wrapper = createComponent(); + + findSavedReplyNameInput().setValue('Test'); + findSavedReplyContentInput().setValue('Test content'); + findSavedReplyFrom().trigger('submit'); + + await nextTick(); + + expect(findSubmitBtn().props('loading')).toBe(true); + + await waitForPromises(); + + expect(findSubmitBtn().props('loading')).toBe(false); + }); + }); +}); diff --git a/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb b/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb index ba201d93f52..40ab9cb2dd2 100644 --- a/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb +++ b/spec/lib/gitlab/database/async_foreign_keys/postgres_async_foreign_key_validation_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Database::AsyncForeignKeys::PostgresAsyncForeignKeyValida subject { fk_validation } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:table_name) } it { is_expected.to validate_length_of(:name).is_at_most(identifier_limit) } it { is_expected.to validate_presence_of(:table_name) } it { is_expected.to validate_length_of(:table_name).is_at_most(identifier_limit) } diff --git a/spec/requests/api/graphql/project/flow_metrics_spec.rb b/spec/requests/api/graphql/project/flow_metrics_spec.rb new file mode 100644 index 00000000000..0bdf7bad8db --- /dev/null +++ b/spec/requests/api/graphql/project/flow_metrics_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting project flow metrics', feature_category: :value_stream_management do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project1) { create(:project, group: group) } + # This is done so we can use the same count expectations in the shared examples and + # reuse the shared example for the group-level test. + let_it_be(:project2) { project1 } + let_it_be(:current_user) { create(:user, maintainer_projects: [project1]) } + + it_behaves_like 'value stream analytics flow metrics issueCount examples' do + let(:full_path) { project1.full_path } + let(:context) { :project } + end +end diff --git a/spec/services/system_notes/commit_service_spec.rb b/spec/services/system_notes/commit_service_spec.rb index 0399603980d..8dfb83f63fe 100644 --- a/spec/services/system_notes/commit_service_spec.rb +++ b/spec/services/system_notes/commit_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe SystemNotes::CommitService do +RSpec.describe SystemNotes::CommitService, feature_category: :code_review_workflow do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, group: group) } let_it_be(:author) { create(:user) } @@ -13,7 +13,7 @@ RSpec.describe SystemNotes::CommitService do subject { commit_service.add_commits(new_commits, old_commits, oldrev) } let(:noteable) { create(:merge_request, source_project: project, target_project: project) } - let(:new_commits) { noteable.commits } + let(:new_commits) { create_commits(10) } let(:old_commits) { [] } let(:oldrev) { nil } @@ -43,6 +43,48 @@ RSpec.describe SystemNotes::CommitService do expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>") end end + + context 'with HTML content' do + let(:new_commits) { [double(title: '<pre>This is a test</pre>', short_id: '12345678')] } + + it 'escapes HTML titles' do + expect(note_lines[1]).to eq("<ul><li>12345678 - <pre>This is a test</pre></li></ul>") + end + end + + context 'with one commit exceeding the NEW_COMMIT_DISPLAY_LIMIT' do + let(:new_commits) { create_commits(11) } + let(:earlier_commit_summary_line) { note_lines[1] } + + it 'includes the truncated new commits summary' do + expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id} - 1 earlier commit") + end + + context 'with oldrev' do + let(:oldrev) { '12345678abcd' } + + it 'includes the truncated new commits summary with the oldrev' do + expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id} - 1 earlier commit") + end + end + end + + context 'with multiple commits exceeding the NEW_COMMIT_DISPLAY_LIMIT' do + let(:new_commits) { create_commits(13) } + let(:earlier_commit_summary_line) { note_lines[1] } + + it 'includes the truncated new commits summary' do + expect(earlier_commit_summary_line).to start_with("<ul><li>#{new_commits[0].short_id}..#{new_commits[2].short_id} - 3 earlier commits") + end + + context 'with oldrev' do + let(:oldrev) { '12345678abcd' } + + it 'includes the truncated new commits summary with the oldrev' do + expect(earlier_commit_summary_line).to start_with("<ul><li>12345678...#{new_commits[2].short_id} - 3 earlier commits") + end + end + end end describe 'summary line for existing commits' do @@ -54,6 +96,15 @@ RSpec.describe SystemNotes::CommitService do it 'includes the existing commit' do expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>") end + + context 'with new commits exceeding the display limit' do + let(:summary_line) { note_lines[1] } + let(:new_commits) { create_commits(13) } + + it 'includes the existing commit as well as the truncated new commit summary' do + expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits") + end + end end context 'with multiple existing commits' do @@ -66,6 +117,15 @@ RSpec.describe SystemNotes::CommitService do expect(summary_line) .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>") end + + context 'with new commits exceeding the display limit' do + let(:new_commits) { create_commits(13) } + + it 'includes the existing commit as well as the truncated new commit summary' do + expect(summary_line) + .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits") + end + end end context 'without oldrev' do @@ -73,6 +133,15 @@ RSpec.describe SystemNotes::CommitService do expect(summary_line) .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>") end + + context 'with new commits exceeding the display limit' do + let(:new_commits) { create_commits(13) } + + it 'includes the existing commit as well as the truncated new commit summary' do + expect(summary_line) + .to start_with("<ul><li>#{old_commits.first.short_id}..#{old_commits.last.short_id} - 26 commits from branch <code>feature</code></li><li>#{old_commits.last.short_id}...#{new_commits[2].short_id} - 3 earlier commits") + end + end end context 'on a fork' do @@ -106,12 +175,9 @@ RSpec.describe SystemNotes::CommitService do end end - describe '#new_commit_summary' do - it 'escapes HTML titles' do - commit = double(title: '<pre>This is a test</pre>', short_id: '12345678') - escaped = '<pre>This is a test</pre>' - - expect(described_class.new.new_commit_summary([commit])).to all(match(/- #{escaped}/)) + def create_commits(count) + Array.new(count) do |i| + double(title: "Test commit #{i}", short_id: "abcd00#{i}") end end end diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb new file mode 100644 index 00000000000..046036c40ba --- /dev/null +++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'value stream analytics flow metrics issueCount examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + let_it_be(:issue1) { create(:issue, project: project1, author: author, created_at: 12.days.ago) } + let_it_be(:issue2) { create(:issue, project: project2, author: author, created_at: 13.days.ago) } + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 15.days.ago) + end + + let_it_be(:issue_outside_of_the_range) { create(:issue, project: project2, author: author, created_at: 50.days.ago) } + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + issueCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'issueCount') + end + + it 'returns the correct count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 4, + 'title' => n_('New Issue', 'New Issues', 4) + }) + end + + context 'with partial filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 2, + 'title' => n_('New Issue', 'New Issues', 2) + }) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to eq({ + 'identifier' => 'issues', + 'unit' => nil, + 'value' => 1, + 'title' => n_('New Issue', 'New Issues', 1) + }) + end + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end +end |