diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-08 15:08:46 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-06-08 15:08:46 +0300 |
commit | cdda3d117c99cadf295f26abc92cb2456033b762 (patch) | |
tree | 30315b1ea79ee4639f44a407978ed719c62a1653 /spec | |
parent | f4ea1f8998fd64bcd02280514b91f103f830d5ce (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
24 files changed, 1987 insertions, 1511 deletions
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 3f5a55410d2..704171a737b 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -3,1453 +3,7 @@ require 'spec_helper' RSpec.describe IssuesFinder do - using RSpec::Parameterized::TableSyntax include_context 'IssuesFinder context' - describe '#execute' do - include_context 'IssuesFinder#execute context' - - context 'scope: all' do - let(:scope) { 'all' } - - it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5) - end - - context 'user does not have read permissions' do - let(:search_user) { user2 } - - context 'when filtering by project id' do - let(:params) { { project_id: project1.id } } - - it 'returns no issues' do - expect(issues).to be_empty - end - end - - context 'when filtering by group id' do - let(:params) { { group_id: group.id } } - - it 'returns no issues' do - expect(issues).to be_empty - end - end - end - - context 'assignee filtering' do - let(:issuables) { issues } - - it_behaves_like 'assignee ID filter' do - let(:params) { { assignee_id: user.id } } - let(:expected_issuables) { [issue1, issue2, issue5] } - end - - it_behaves_like 'assignee NOT ID filter' do - let(:params) { { not: { assignee_id: user.id } } } - let(:expected_issuables) { [issue3, issue4] } - end - - it_behaves_like 'assignee OR filter' do - let(:params) { { or: { assignee_id: [user.id, user2.id] } } } - let(:expected_issuables) { [issue1, issue2, issue3, issue5] } - end - - context 'when assignee_id does not exist' do - it_behaves_like 'assignee NOT ID filter' do - let(:params) { { not: { assignee_id: -100 } } } - let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] } - end - end - - context 'filter by username' do - let_it_be(:user3) { create(:user) } - - before do - project2.add_developer(user3) - issue2.assignees = [user2] - issue3.assignees = [user3] - end - - it_behaves_like 'assignee username filter' do - let(:params) { { assignee_username: [user2.username] } } - let(:expected_issuables) { [issue2] } - end - - it_behaves_like 'assignee NOT username filter' do - before do - issue2.assignees = [user2] - end - - let(:params) { { not: { assignee_username: [user.username, user2.username] } } } - let(:expected_issuables) { [issue3, issue4] } - end - - it_behaves_like 'assignee OR filter' do - let(:params) { { or: { assignee_username: [user2.username, user3.username] } } } - let(:expected_issuables) { [issue2, issue3] } - end - - context 'when assignee_username does not exist' do - it_behaves_like 'assignee NOT username filter' do - before do - issue2.assignees = [user2] - end - - let(:params) { { not: { assignee_username: 'non_existent_username' } } } - let(:expected_issuables) { [issue1, issue2, issue3, issue4, issue5] } - end - end - end - - it_behaves_like 'no assignee filter' do - let_it_be(:user3) { create(:user) } - let(:expected_issuables) { [issue4] } - end - - it_behaves_like 'any assignee filter' do - let(:expected_issuables) { [issue1, issue2, issue3, issue5] } - end - end - - context 'filtering by release' do - context 'when the release tag is none' do - let(:params) { { release_tag: 'none' } } - - it 'returns issues without releases' do - expect(issues).to contain_exactly(issue2, issue3, issue4, issue5) - end - end - - context 'when the release tag exists' do - let(:params) { { project_id: project1.id, release_tag: release.tag } } - - it 'returns the issues associated with that release' do - expect(issues).to contain_exactly(issue1) - end - end - end - - context 'filtering by projects' do - context 'when projects are passed in a list of ids' do - let(:params) { { projects: [project1.id] } } - - it 'returns the issue belonging to the projects' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - - context 'when projects are passed in a subquery' do - let(:params) { { projects: Project.id_in(project1.id) } } - - it 'returns the issue belonging to the projects' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - end - - context 'filtering by group_id' do - let(:params) { { group_id: group.id } } - - context 'when include_subgroup param not set' do - it 'returns all group issues' do - expect(issues).to contain_exactly(issue1, issue5) - end - - context 'when projects outside the group are passed' do - let(:params) { { group_id: group.id, projects: [project2.id] } } - - it 'returns no issues' do - expect(issues).to be_empty - end - end - - context 'when projects of the group are passed' do - let(:params) { { group_id: group.id, projects: [project1.id] } } - - it 'returns the issue within the group and projects' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - - context 'when projects of the group are passed as a subquery' do - let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } } - - it 'returns the issue within the group and projects' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - - context 'when release_tag is passed as a parameter' do - let(:params) { { group_id: group.id, release_tag: 'dne-release-tag' } } - - it 'ignores the release_tag parameter' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - end - - context 'when include_subgroup param is true' do - before do - params[:include_subgroups] = true - end - - it 'returns all group and subgroup issues' do - expect(issues).to contain_exactly(issue1, issue4, issue5) - end - - context 'when mixed projects are passed' do - let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } } - - it 'returns the issue within the group and projects' do - expect(issues).to contain_exactly(issue4) - end - end - end - end - - context 'filtering by author' do - context 'by author ID' do - let(:params) { { author_id: user2.id } } - - it 'returns issues created by that user' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'using OR' do - let(:issue6) { create(:issue, project: project2) } - let(:params) { { or: { author_username: [issue3.author.username, issue6.author.username] } } } - - it 'returns issues created by any of the given users' do - expect(issues).to contain_exactly(issue3, issue6) - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(or_issuable_queries: false) - end - - it 'does not add any filter' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5, issue6) - end - end - end - - context 'filtering by NOT author ID' do - let(:params) { { not: { author_id: user2.id } } } - - it 'returns issues not created by that user' do - expect(issues).to contain_exactly(issue1, issue2, issue4, issue5) - end - end - - context 'filtering by nonexistent author ID and issue term using CTE for search' do - let(:params) do - { - author_id: 'does-not-exist', - search: 'git', - attempt_group_search_optimizations: true - } - end - - it 'returns no results' do - expect(issues).to be_empty - end - end - end - - context 'filtering by milestone' do - let(:params) { { milestone_title: milestone.title } } - - it 'returns issues assigned to that milestone' do - expect(issues).to contain_exactly(issue1) - end - end - - context 'filtering by not milestone' do - let(:params) { { not: { milestone_title: milestone.title } } } - - it 'returns issues not assigned to that milestone' do - expect(issues).to contain_exactly(issue2, issue3, issue4, issue5) - end - end - - context 'filtering by group milestone' do - let!(:group) { create(:group, :public) } - let(:group_milestone) { create(:milestone, group: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let(:params) { { milestone_title: group_milestone.title } } - - before do - project2.update!(namespace: group) - issue2.update!(milestone: group_milestone) - issue3.update!(milestone: group_milestone) - end - - it 'returns issues assigned to that group milestone' do - expect(issues).to contain_exactly(issue2, issue3) - end - - context 'using NOT' do - let(:params) { { not: { milestone_title: group_milestone.title } } } - - it 'returns issues not assigned to that group milestone' do - expect(issues).to contain_exactly(issue1, issue4, issue5) - end - end - end - - context 'filtering by no milestone' do - let(:params) { { milestone_title: 'None' } } - - it 'returns issues with no milestone' do - expect(issues).to contain_exactly(issue2, issue3, issue4, issue5) - end - - it 'returns issues with no milestone (deprecated)' do - params[:milestone_title] = Milestone::None.title - - expect(issues).to contain_exactly(issue2, issue3, issue4, issue5) - end - end - - context 'filtering by any milestone' do - let(:params) { { milestone_title: 'Any' } } - - it 'returns issues with any assigned milestone' do - expect(issues).to contain_exactly(issue1) - end - - it 'returns issues with any assigned milestone (deprecated)' do - params[:milestone_title] = Milestone::Any.title - - expect(issues).to contain_exactly(issue1) - end - end - - context 'filtering by upcoming milestone' do - let(:params) { { milestone_title: Milestone::Upcoming.name } } - - let!(:group) { create(:group, :public) } - let!(:group_member) { create(:group_member, group: group, user: user) } - - let(:project_no_upcoming_milestones) { create(:project, :public) } - let(:project_next_1_1) { create(:project, :public) } - let(:project_next_8_8) { create(:project, :public) } - let(:project_in_group) { create(:project, :public, namespace: group) } - - let(:yesterday) { Date.current - 1.day } - let(:tomorrow) { Date.current + 1.day } - let(:two_days_from_now) { Date.current + 2.days } - let(:ten_days_from_now) { Date.current + 10.days } - - let(:milestones) do - [ - create(:milestone, :closed, project: project_no_upcoming_milestones), - create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), - create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now), - create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday), - create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow), - create(:milestone, group: group, title: '9.9', due_date: tomorrow) - ] - end - - before do - @created_issues = milestones.map do |milestone| - create(:issue, project: milestone.project || project_in_group, milestone: milestone, author: user, assignees: [user]) - end - end - - it 'returns issues in the upcoming milestone for each project or group' do - expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.1', '8.8', '9.9') - expect(issues.map { |issue| issue.milestone.due_date }).to contain_exactly(tomorrow, two_days_from_now, tomorrow) - end - - context 'using NOT' do - let(:params) { { not: { milestone_title: Milestone::Upcoming.name } } } - - it 'returns issues not in upcoming milestones for each project or group, but must have a due date' do - target_issues = @created_issues.select do |issue| - issue.milestone&.due_date && issue.milestone.due_date <= Date.current - end - - expect(issues).to contain_exactly(*target_issues) - end - end - end - - context 'filtering by started milestone' do - let(:params) { { milestone_title: Milestone::Started.name } } - - let(:project_no_started_milestones) { create(:project, :public) } - let(:project_started_1_and_2) { create(:project, :public) } - let(:project_started_8) { create(:project, :public) } - - let(:yesterday) { Date.current - 1.day } - let(:tomorrow) { Date.current + 1.day } - let(:two_days_ago) { Date.current - 2.days } - let(:three_days_ago) { Date.current - 3.days } - - let(:milestones) do - [ - create(:milestone, project: project_no_started_milestones, start_date: tomorrow), - create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago), - create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday), - create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow), - create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago), - create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago), - create(:milestone, project: project_started_8, title: '7.0'), - create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday), - create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow) - ] - end - - before do - milestones.each do |milestone| - create(:issue, project: milestone.project, milestone: milestone, author: user, assignees: [user]) - end - end - - it 'returns issues in the started milestones for each project' do - expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0') - expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday) - end - - context 'using NOT' do - let(:params) { { not: { milestone_title: Milestone::Started.name } } } - - it 'returns issues not in the started milestones for each project' do - target_issues = Issue.where(milestone: Milestone.not_started) - - expect(issues).to contain_exactly(*target_issues) - end - end - end - - context 'filtering by label' do - let(:params) { { label_name: label.title } } - - it 'returns issues with that label' do - expect(issues).to contain_exactly(issue2) - end - - context 'using NOT' do - let(:params) { { not: { label_name: label.title } } } - - it 'returns issues that do not have that label' do - expect(issues).to contain_exactly(issue1, issue3, issue4, issue5) - end - - # IssuableFinder first filters using the outer params (the ones not inside the `not` key.) - # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param - # do not take precedence over the outer params with the same name. - context 'shadowing the same outside param' do - let(:params) { { label_name: label2.title, not: { label_name: label.title } } } - - it 'does not take precedence over labels outside NOT' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'further filtering outside params' do - let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } } - - it 'further filters on the returned resultset' do - expect(issues).to be_empty - end - end - end - end - - context 'filtering by multiple labels' do - let(:params) { { label_name: [label.title, label2.title].join(',') } } - let(:label2) { create(:label, project: project2) } - - before do - create(:label_link, label: label2, target: issue2) - end - - it 'returns the unique issues with all those labels' do - expect(issues).to contain_exactly(issue2) - end - - context 'using NOT' do - let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } - - it 'returns issues that do not have any of the labels provided' do - expect(issues).to contain_exactly(issue1, issue4, issue5) - end - end - end - - context 'filtering by a label that includes any or none in the title' do - let(:params) { { label_name: [label.title, label2.title].join(',') } } - let(:label) { create(:label, title: 'any foo', project: project2) } - let(:label2) { create(:label, title: 'bar none', project: project2) } - - before do - create(:label_link, label: label2, target: issue2) - end - - it 'returns the unique issues with all those labels' do - expect(issues).to contain_exactly(issue2) - end - - context 'using NOT' do - let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } - - it 'returns issues that do not have ANY ONE of the labels provided' do - expect(issues).to contain_exactly(issue1, issue4, issue5) - end - end - end - - context 'filtering by no label' do - let(:params) { { label_name: described_class::Params::FILTER_NONE } } - - it 'returns issues with no labels' do - expect(issues).to contain_exactly(issue1, issue4, issue5) - end - end - - context 'filtering by any label' do - let(:params) { { label_name: described_class::Params::FILTER_ANY } } - - it 'returns issues that have one or more label' do - create_list(:label_link, 2, label: create(:label, project: project2), target: issue3) - - expect(issues).to contain_exactly(issue2, issue3) - end - end - - context 'when the same label exists on project and group levels' do - let(:issue1) { create(:issue, project: project1) } - let(:issue2) { create(:issue, project: project1) } - - # Skipping validation to reproduce a "real-word" scenario. - # We still have legacy labels on PRD that have the same title on the group and project levels, example: `bug` - let(:project_label) { build(:label, title: 'somelabel', project: project1).tap { |r| r.save!(validate: false) } } - let(:group_label) { create(:group_label, title: 'somelabel', group: project1.group) } - - let(:params) { { label_name: 'somelabel' } } - - before do - create(:label_link, label: group_label, target: issue1) - create(:label_link, label: project_label, target: issue2) - end - - it 'finds both issue records' do - expect(issues).to contain_exactly(issue1, issue2) - end - end - - context 'filtering by issue term' do - let(:params) { { search: search_term } } - - let_it_be(:english) { create(:issue, project: project1, title: 'title', description: 'something english') } - let_it_be(:japanese) { create(:issue, project: project1, title: '日本語 title', description: 'another english description') } - - context 'with latin search term' do - let(:search_term) { 'title english' } - - it 'returns matching issues' do - expect(issues).to contain_exactly(english, japanese) - end - end - - context 'with non-latin search term' do - let(:search_term) { '日本語' } - - it 'returns matching issues' do - expect(issues).to contain_exactly(japanese) - end - end - - context 'when full-text search is disabled' do - let(:search_term) { 'somet' } - - before do - stub_feature_flags(issues_full_text_search: false) - end - - it 'allows partial word matches' do - expect(issues).to contain_exactly(english) - end - end - - context 'with anonymous user' do - let_it_be(:public_project) { create(:project, :public, group: subgroup) } - let_it_be(:issue6) { create(:issue, project: public_project, title: 'tanuki') } - let_it_be(:issue7) { create(:issue, project: public_project, title: 'ikunat') } - - let(:search_user) { nil } - let(:params) { { search: 'tanuki' } } - - context 'with disable_anonymous_search feature flag enabled' do - before do - stub_feature_flags(disable_anonymous_search: true) - end - - it 'does not perform search' do - expect(issues).to contain_exactly(issue6, issue7) - end - end - - context 'with disable_anonymous_search feature flag disabled' do - before do - stub_feature_flags(disable_anonymous_search: false) - end - - it 'finds one public issue' do - expect(issues).to contain_exactly(issue6) - end - end - end - end - - context 'filtering by issue term in title' do - let(:params) { { search: 'git', in: 'title' } } - - it 'returns issues with title match for search term' do - expect(issues).to contain_exactly(issue1) - end - end - - context 'filtering by issues iids' do - let(:params) { { iids: [issue3.iid] } } - - it 'returns issues where iids match' do - expect(issues).to contain_exactly(issue3, issue5) - end - - context 'using NOT' do - let(:params) { { not: { iids: [issue3.iid] } } } - - it 'returns issues with no iids match' do - expect(issues).to contain_exactly(issue1, issue2, issue4) - end - end - end - - context 'filtering by state' do - context 'with opened' do - let(:params) { { state: 'opened' } } - - it 'returns only opened issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5) - end - end - - context 'with closed' do - let(:params) { { state: 'closed' } } - - it 'returns only closed issues' do - expect(issues).to contain_exactly(closed_issue) - end - end - - context 'with all' do - let(:params) { { state: 'all' } } - - it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4, issue5) - end - end - - context 'with invalid state' do - let(:params) { { state: 'invalid_state' } } - - it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4, issue5) - end - end - end - - context 'filtering by created_at' do - context 'through created_after' do - let(:params) { { created_after: issue3.created_at } } - - it 'returns issues created on or after the given date' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'through created_before' do - let(:params) { { created_before: issue1.created_at } } - - it 'returns issues created on or before the given date' do - expect(issues).to contain_exactly(issue1) - end - end - - context 'through created_after and created_before' do - let(:params) { { created_after: issue2.created_at, created_before: issue3.created_at } } - - it 'returns issues created between the given dates' do - expect(issues).to contain_exactly(issue2, issue3) - end - end - end - - context 'filtering by updated_at' do - context 'through updated_after' do - let(:params) { { updated_after: issue3.updated_at } } - - it 'returns issues updated on or after the given date' do - expect(issues).to contain_exactly(issue3) - end - end - - context 'through updated_before' do - let(:params) { { updated_before: issue1.updated_at } } - - it 'returns issues updated on or before the given date' do - expect(issues).to contain_exactly(issue1) - end - end - - context 'through updated_after and updated_before' do - let(:params) { { updated_after: issue2.updated_at, updated_before: issue3.updated_at } } - - it 'returns issues updated between the given dates' do - expect(issues).to contain_exactly(issue2, issue3) - end - end - end - - context 'filtering by closed_at' do - let!(:closed_issue1) { create(:issue, project: project1, state: :closed, closed_at: 1.week.ago) } - let!(:closed_issue2) { create(:issue, project: project2, state: :closed, closed_at: 1.week.from_now) } - let!(:closed_issue3) { create(:issue, project: project2, state: :closed, closed_at: 2.weeks.from_now) } - - context 'through closed_after' do - let(:params) { { state: :closed, closed_after: closed_issue3.closed_at } } - - it 'returns issues closed on or after the given date' do - expect(issues).to contain_exactly(closed_issue3) - end - end - - context 'through closed_before' do - let(:params) { { state: :closed, closed_before: closed_issue1.closed_at } } - - it 'returns issues closed on or before the given date' do - expect(issues).to contain_exactly(closed_issue1) - end - end - - context 'through closed_after and closed_before' do - let(:params) { { state: :closed, closed_after: closed_issue2.closed_at, closed_before: closed_issue3.closed_at } } - - it 'returns issues closed between the given dates' do - expect(issues).to contain_exactly(closed_issue2, closed_issue3) - end - end - end - - context 'filtering by reaction name' do - context 'user searches by no reaction' do - let(:params) { { my_reaction_emoji: 'None' } } - - it 'returns issues that the user did not react to' do - expect(issues).to contain_exactly(issue2, issue4, issue5) - end - end - - context 'user searches by any reaction' do - let(:params) { { my_reaction_emoji: 'Any' } } - - it 'returns issues that the user reacted to' do - expect(issues).to contain_exactly(issue1, issue3) - end - end - - context 'user searches by "thumbsup" reaction' do - let(:params) { { my_reaction_emoji: 'thumbsup' } } - - it 'returns issues that the user thumbsup to' do - expect(issues).to contain_exactly(issue1) - end - - context 'using NOT' do - let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } } - - it 'returns issues that the user did not thumbsup to' do - expect(issues).to contain_exactly(issue2, issue3, issue4, issue5) - end - end - end - - context 'user2 searches by "thumbsup" reaction' do - let(:search_user) { user2 } - - let(:params) { { my_reaction_emoji: 'thumbsup' } } - - it 'returns issues that the user2 thumbsup to' do - expect(issues).to contain_exactly(issue2) - end - - context 'using NOT' do - let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } } - - it 'returns issues that the user2 thumbsup to' do - expect(issues).to contain_exactly(issue3) - end - end - end - - context 'user searches by "thumbsdown" reaction' do - let(:params) { { my_reaction_emoji: 'thumbsdown' } } - - it 'returns issues that the user thumbsdown to' do - expect(issues).to contain_exactly(issue3) - end - - context 'using NOT' do - let(:params) { { not: { my_reaction_emoji: 'thumbsdown' } } } - - it 'returns issues that the user thumbsdown to' do - expect(issues).to contain_exactly(issue1, issue2, issue4, issue5) - end - end - end - end - - context 'filtering by confidential' do - let_it_be(:confidential_issue) { create(:issue, project: project1, confidential: true) } - - context 'no filtering' do - it 'returns all issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5, confidential_issue) - end - end - - context 'user filters confidential issues' do - let(:params) { { confidential: true } } - - it 'returns only confidential issues' do - expect(issues).to contain_exactly(confidential_issue) - end - end - - context 'user filters only public issues' do - let(:params) { { confidential: false } } - - it 'returns only public issues' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5) - end - end - end - - context 'filtering by issue type' do - let_it_be(:incident_issue) { create(:incident, project: project1) } - - context 'no type given' do - let(:params) { { issue_types: [] } } - - it 'returns all issues' do - expect(issues).to contain_exactly(incident_issue, issue1, issue2, issue3, issue4, issue5) - end - end - - context 'incident type' do - let(:params) { { issue_types: ['incident'] } } - - it 'returns incident issues' do - expect(issues).to contain_exactly(incident_issue) - end - end - - context 'issue type' do - let(:params) { { issue_types: ['issue'] } } - - it 'returns all issues with type issue' do - expect(issues).to contain_exactly(issue1, issue2, issue3, issue4, issue5) - end - end - - context 'multiple params' do - let(:params) { { issue_types: %w(issue incident) } } - - it 'returns all issues' do - expect(issues).to contain_exactly(incident_issue, issue1, issue2, issue3, issue4, issue5) - end - end - - context 'without array' do - let(:params) { { issue_types: 'incident' } } - - it 'returns incident issues' do - expect(issues).to contain_exactly(incident_issue) - end - end - - context 'invalid params' do - let(:params) { { issue_types: ['nonsense'] } } - - it 'returns no issues' do - expect(issues).to eq(Issue.none) - end - end - end - - context 'filtering by crm contact' do - let_it_be(:contact1) { create(:contact, group: group) } - let_it_be(:contact2) { create(:contact, group: group) } - - let_it_be(:contact1_issue1) { create(:issue, project: project1) } - let_it_be(:contact1_issue2) { create(:issue, project: project1) } - let_it_be(:contact2_issue1) { create(:issue, project: project1) } - - let(:params) { { crm_contact_id: contact1.id } } - - it 'returns for that contact' do - create(:issue_customer_relations_contact, issue: contact1_issue1, contact: contact1) - create(:issue_customer_relations_contact, issue: contact1_issue2, contact: contact1) - create(:issue_customer_relations_contact, issue: contact2_issue1, contact: contact2) - - expect(issues).to contain_exactly(contact1_issue1, contact1_issue2) - end - end - - context 'filtering by crm organization' do - let_it_be(:organization) { create(:organization, group: group) } - let_it_be(:contact1) { create(:contact, group: group, organization: organization) } - let_it_be(:contact2) { create(:contact, group: group, organization: organization) } - - let_it_be(:contact1_issue1) { create(:issue, project: project1) } - let_it_be(:contact1_issue2) { create(:issue, project: project1) } - let_it_be(:contact2_issue1) { create(:issue, project: project1) } - - let(:params) { { crm_organization_id: organization.id } } - - it 'returns for that contact' do - create(:issue_customer_relations_contact, issue: contact1_issue1, contact: contact1) - create(:issue_customer_relations_contact, issue: contact1_issue2, contact: contact1) - create(:issue_customer_relations_contact, issue: contact2_issue1, contact: contact2) - - expect(issues).to contain_exactly(contact1_issue1, contact1_issue2, contact2_issue1) - end - end - - context 'when the user is unauthorized' do - let(:search_user) { nil } - - it 'returns no results' do - expect(issues).to be_empty - end - end - - context 'when the user can see some, but not all, issues' do - let(:search_user) { user2 } - - it 'returns only issues they can see' do - expect(issues).to contain_exactly(issue2, issue3) - end - end - - it 'finds issues user can access due to group' do - group = create(:group) - project = create(:project, group: group) - issue = create(:issue, project: project) - group.add_user(user, :owner) - - expect(issues).to include(issue) - end - end - - context 'personal scope' do - let(:scope) { 'assigned_to_me' } - - it 'returns issue assigned to the user' do - expect(issues).to contain_exactly(issue1, issue2, issue5) - end - - context 'filtering by project' do - let(:params) { { project_id: project1.id } } - - it 'returns issues assigned to the user in that project' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - end - - context 'when project restricts issues' do - let(:scope) { nil } - - it "doesn't return team-only issues to non team members" do - project = create(:project, :public, :issues_private) - issue = create(:issue, project: project) - - expect(issues).not_to include(issue) - end - - it "doesn't return issues if feature disabled" do - [project1, project2, project3].each do |project| - project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) - end - - expect(issues.count).to eq 0 - end - end - - context 'external authorization' do - it_behaves_like 'a finder with external authorization service' do - let!(:subject) { create(:issue, project: project) } - let(:project_params) { { project_id: project.id } } - end - end - - context 'filtering by due date' do - let_it_be(:issue_due_today) { create(:issue, project: project1, due_date: Date.current) } - let_it_be(:issue_due_tomorrow) { create(:issue, project: project1, due_date: 1.day.from_now) } - let_it_be(:issue_overdue) { create(:issue, project: project1, due_date: 2.days.ago) } - let_it_be(:issue_due_soon) { create(:issue, project: project1, due_date: 2.days.from_now) } - - let(:scope) { 'all' } - let(:base_params) { { project_id: project1.id } } - - context 'with param set to no due date' do - let(:params) { base_params.merge(due_date: Issue::NoDueDate.name) } - - it 'returns issues with no due date' do - expect(issues).to contain_exactly(issue1, issue5) - end - end - - context 'with param set to any due date' do - let(:params) { base_params.merge(due_date: Issue::AnyDueDate.name) } - - it 'returns issues with any due date' do - expect(issues).to contain_exactly(issue_due_today, issue_due_tomorrow, issue_overdue, issue_due_soon) - end - end - - context 'with param set to due today' do - let(:params) { base_params.merge(due_date: Issue::DueToday.name) } - - it 'returns issues due today' do - expect(issues).to contain_exactly(issue_due_today) - end - end - - context 'with param set to due tomorrow' do - let(:params) { base_params.merge(due_date: Issue::DueTomorrow.name) } - - it 'returns issues due today' do - expect(issues).to contain_exactly(issue_due_tomorrow) - end - end - - context 'with param set to overdue' do - let(:params) { base_params.merge(due_date: Issue::Overdue.name) } - - it 'returns overdue issues' do - expect(issues).to contain_exactly(issue_overdue) - end - end - - context 'with param set to next month and previous two weeks' do - let(:params) { base_params.merge(due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name) } - - it 'returns issues due in the previous two weeks and next month' do - expect(issues).to contain_exactly(issue_due_today, issue_due_tomorrow, issue_overdue, issue_due_soon) - end - end - - context 'with invalid param' do - let(:params) { base_params.merge(due_date: 'foo') } - - it 'returns no issues' do - expect(issues).to be_empty - end - end - end - end - - describe '#row_count', :request_store do - let_it_be(:admin) { create(:admin) } - - context 'when admin mode is enabled', :enable_admin_mode do - it 'returns the number of rows for the default state' do - finder = described_class.new(admin) - - expect(finder.row_count).to eq(5) - end - - it 'returns the number of rows for a given state' do - finder = described_class.new(admin, state: 'closed') - - expect(finder.row_count).to be_zero - end - end - - context 'when admin mode is disabled' do - it 'returns no rows' do - finder = described_class.new(admin) - - expect(finder.row_count).to be_zero - end - end - - it 'returns -1 if the query times out' do - finder = described_class.new(admin) - - expect_next_instance_of(described_class) do |subfinder| - expect(subfinder).to receive(:execute).and_raise(ActiveRecord::QueryCanceled) - end - - expect(finder.row_count).to eq(-1) - end - end - - describe '#with_confidentiality_access_check' do - let(:guest) { create(:user) } - - let_it_be(:authorized_user) { create(:user) } - let_it_be(:banned_user) { create(:user, :banned) } - let_it_be(:project) { create(:project, namespace: authorized_user.namespace) } - let_it_be(:public_issue) { create(:issue, project: project) } - let_it_be(:confidential_issue) { create(:issue, project: project, confidential: true) } - let_it_be(:hidden_issue) { create(:issue, project: project, author: banned_user) } - - shared_examples 'returns public, does not return hidden or confidential' do - it 'returns only public issues' do - expect(subject).to include(public_issue) - expect(subject).not_to include(confidential_issue, hidden_issue) - end - end - - shared_examples 'returns public and confidential, does not return hidden' do - it 'returns only public and confidential issues' do - expect(subject).to include(public_issue, confidential_issue) - expect(subject).not_to include(hidden_issue) - end - end - - shared_examples 'returns public and hidden, does not return confidential' do - it 'returns only public and hidden issues' do - expect(subject).to include(public_issue, hidden_issue) - expect(subject).not_to include(confidential_issue) - end - end - - shared_examples 'returns public, confidential, and hidden' do - it 'returns all issues' do - expect(subject).to include(public_issue, confidential_issue, hidden_issue) - end - end - - context 'when no project filter is given' do - let(:params) { {} } - - context 'for an anonymous user' do - subject { described_class.new(nil, params).with_confidentiality_access_check } - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - end - - context 'for a user without project membership' do - subject { described_class.new(user, params).with_confidentiality_access_check } - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - end - - context 'for a guest user' do - subject { described_class.new(guest, params).with_confidentiality_access_check } - - before do - project.add_guest(guest) - end - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - end - - context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).with_confidentiality_access_check } - - it_behaves_like 'returns public and confidential, does not return hidden' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public, confidential, and hidden' - end - end - - context 'for an admin' do - let(:admin_user) { create(:user, :admin) } - - subject { described_class.new(admin_user, params).with_confidentiality_access_check } - - context 'when admin mode is enabled', :enable_admin_mode do - it_behaves_like 'returns public, confidential, and hidden' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public, confidential, and hidden' - end - end - - context 'when admin mode is disabled' do - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - end - end - end - - context 'when searching within a specific project' do - let(:params) { { project_id: project.id } } - - context 'for an anonymous user' do - subject { described_class.new(nil, params).with_confidentiality_access_check } - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - - it 'does not filter by confidentiality' do - expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) - subject - end - end - - context 'for a user without project membership' do - subject { described_class.new(user, params).with_confidentiality_access_check } - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - - it 'filters by confidentiality' do - expect(subject.to_sql).to match("issues.confidential") - end - end - - context 'for a guest user' do - subject { described_class.new(guest, params).with_confidentiality_access_check } - - before do - project.add_guest(guest) - end - - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - - it 'filters by confidentiality' do - expect(subject.to_sql).to match("issues.confidential") - end - end - - context 'for a project member with access to view confidential issues' do - subject { described_class.new(authorized_user, params).with_confidentiality_access_check } - - it_behaves_like 'returns public and confidential, does not return hidden' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public, confidential, and hidden' - end - - it 'does not filter by confidentiality' do - expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) - - subject - end - end - - context 'for an admin' do - let(:admin_user) { create(:user, :admin) } - - subject { described_class.new(admin_user, params).with_confidentiality_access_check } - - context 'when admin mode is enabled', :enable_admin_mode do - it_behaves_like 'returns public, confidential, and hidden' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public, confidential, and hidden' - end - - it 'does not filter by confidentiality' do - expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) - - subject - end - end - - context 'when admin mode is disabled' do - it_behaves_like 'returns public, does not return hidden or confidential' - - context 'when feature flag is disabled' do - before do - stub_feature_flags(ban_user_feature_flag: false) - end - - it_behaves_like 'returns public and hidden, does not return confidential' - end - - it 'filters by confidentiality' do - expect(subject.to_sql).to match("issues.confidential") - end - end - end - end - end - - describe '#use_cte_for_search?' do - let(:finder) { described_class.new(nil, params) } - - context 'when there is no search param' do - let(:params) { { attempt_group_search_optimizations: true } } - - it 'returns false' do - expect(finder.use_cte_for_search?).to be_falsey - end - end - - context 'when the force_cte param is falsey' do - let(:params) { { search: '日本語' } } - - it 'returns false' do - expect(finder.use_cte_for_search?).to be_falsey - end - end - - context 'when a non-simple sort is given' do - let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'popularity' } } - - it 'returns false' do - expect(finder.use_cte_for_search?).to be_falsey - end - end - - context 'when all conditions are met' do - context "uses group search optimization" do - let(:params) { { search: '日本語', attempt_group_search_optimizations: true } } - - it 'returns true' do - expect(finder.use_cte_for_search?).to be_truthy - expect(finder.execute.to_sql).to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) - end - end - - context "uses project search optimization" do - let(:params) { { search: '日本語', attempt_project_search_optimizations: true } } - - it 'returns true' do - expect(finder.use_cte_for_search?).to be_truthy - expect(finder.execute.to_sql).to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) - end - end - - context 'with simple sort' do - let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'updated_desc' } } - - it 'returns true' do - expect(finder.use_cte_for_search?).to be_truthy - expect(finder.execute.to_sql).to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) - end - end - - context 'with simple sort as a symbol' do - let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: :updated_desc } } - - it 'returns true' do - expect(finder.use_cte_for_search?).to be_truthy - expect(finder.execute.to_sql).to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) - end - end - end - end - - describe '#parent_param=' do - let(:finder) { described_class.new(nil) } - - subject { finder.parent_param = obj } - - where(:klass, :param) do - :Project | :project_id - :Group | :group_id - end - - with_them do - let(:obj) { Object.const_get(klass, false).new } - - it 'sets the params' do - subject - - expect(finder.params[param]).to eq(obj) - end - end - - context 'unexpected parent' do - let(:obj) { MergeRequest.new } - - it 'raises an error' do - expect { subject }.to raise_error('Unexpected parent: MergeRequest') - end - end - end + it_behaves_like 'issues or work items finder', :issue, 'IssuesFinder#execute context' end diff --git a/spec/finders/work_items/work_items_finder_spec.rb b/spec/finders/work_items/work_items_finder_spec.rb new file mode 100644 index 00000000000..fe400688a23 --- /dev/null +++ b/spec/finders/work_items/work_items_finder_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItems::WorkItemsFinder do + using RSpec::Parameterized::TableSyntax + include_context 'WorkItemsFinder context' + + it_behaves_like 'issues or work items finder', :work_item, 'WorkItemsFinder#execute context' +end diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index 010f7b999fc..cc19e90a5fa 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -200,6 +200,30 @@ describe('InviteModalBase', () => { }); }); + describe('when user limit is close on a personal namespace', () => { + beforeEach(() => { + createComponent( + { + closeToLimit: true, + reachedLimit: false, + usersLimitDataset: { membersPath, userNamespace: true }, + }, + { GlModal, GlFormGroup }, + ); + }); + + it('renders correct buttons', () => { + const cancelButton = findCancelButton(); + const actionButton = findActionButton(); + + expect(cancelButton.text()).toBe(INVITE_BUTTON_TEXT_DISABLED); + expect(cancelButton.attributes('href')).toBe(membersPath); + + expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT); + expect(actionButton.attributes('href')).toBe(); // default submit button + }); + }); + describe('when users limit is not reached', () => { const textRegex = /Select a role.+Read more about role permissions Access expiration date \(optional\)/; diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js index 4c9adbfcc44..bbc17932a49 100644 --- a/spec/frontend/invite_members/components/user_limit_notification_spec.js +++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js @@ -14,9 +14,15 @@ describe('UserLimitNotification', () => { const findAlert = () => wrapper.findComponent(GlAlert); - const createComponent = (reachedLimit = false, usersLimitDataset = {}) => { + const createComponent = ( + closeToLimit = false, + reachedLimit = false, + usersLimitDataset = {}, + props = {}, + ) => { wrapper = shallowMountExtended(UserLimitNotification, { propsData: { + closeToLimit, reachedLimit, usersLimitDataset: { freeUsersLimit, @@ -25,6 +31,7 @@ describe('UserLimitNotification', () => { purchasePath: 'purchasePath', ...usersLimitDataset, }, + ...props, }, provide: { name: 'my group' }, stubs: { GlSprintf }, @@ -43,9 +50,26 @@ describe('UserLimitNotification', () => { }); }); + describe('when close to limit with a personal namepace', () => { + beforeEach(() => { + createComponent(true, false, { membersCount: 3, userNamespace: true }); + }); + + it('renders the limit for a personal namespace', () => { + const alert = findAlert(); + + expect(alert.attributes('title')).toEqual( + 'You only have space for 2 more members in your personal projects', + ); + expect(alert.text()).toEqual( + 'To make more space, you can remove members who no longer need access.', + ); + }); + }); + describe('when close to limit', () => { it("renders user's limit notification", () => { - createComponent(false, { membersCount: 3 }); + createComponent(true, false, { membersCount: 3 }); const alert = findAlert(); @@ -61,7 +85,7 @@ describe('UserLimitNotification', () => { describe('when limit is reached', () => { it("renders user's limit notification", () => { - createComponent(true); + createComponent(true, true); const alert = findAlert(); @@ -71,12 +95,12 @@ describe('UserLimitNotification', () => { describe('when free user namespace', () => { it("renders user's limit notification", () => { - createComponent(true, { userNamespace: true }); + createComponent(true, true, { userNamespace: true }); const alert = findAlert(); expect(alert.attributes('title')).toEqual( - "You've reached your 5 members limit for my group", + "You've reached your 5 members limit for your personal projects", ); expect(alert.text()).toEqual(REACHED_LIMIT_MESSAGE); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 1e693184d73..906f4b560f1 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -373,16 +373,16 @@ describe('Linked pipeline', () => { describe('expand button', () => { it.each` - pipelineType | anglePosition | buttonBorderClasses | expanded - ${downstreamProps} | ${'angle-right'} | ${'gl-border-l-0!'} | ${false} - ${downstreamProps} | ${'angle-left'} | ${'gl-border-l-0!'} | ${true} - ${upstreamProps} | ${'angle-left'} | ${'gl-border-r-0!'} | ${false} - ${upstreamProps} | ${'angle-right'} | ${'gl-border-r-0!'} | ${true} + pipelineType | chevronPosition | buttonBorderClasses | expanded + ${downstreamProps} | ${'chevron-lg-right'} | ${'gl-border-l-0!'} | ${false} + ${downstreamProps} | ${'chevron-lg-left'} | ${'gl-border-l-0!'} | ${true} + ${upstreamProps} | ${'chevron-lg-left'} | ${'gl-border-r-0!'} | ${false} + ${upstreamProps} | ${'chevron-lg-right'} | ${'gl-border-r-0!'} | ${true} `( - '$pipelineType.columnTitle pipeline button icon should be $anglePosition with $buttonBorderClasses if expanded state is $expanded', - ({ pipelineType, anglePosition, buttonBorderClasses, expanded }) => { + '$pipelineType.columnTitle pipeline button icon should be $chevronPosition with $buttonBorderClasses if expanded state is $expanded', + ({ pipelineType, chevronPosition, buttonBorderClasses, expanded }) => { createWrapper({ propsData: { ...pipelineType, expanded } }); - expect(findExpandButton().props('icon')).toBe(anglePosition); + expect(findExpandButton().props('icon')).toBe(chevronPosition); expect(findExpandButton().classes()).toContain(buttonBorderClasses); }, ); diff --git a/spec/lib/api/entities/ci/job_request/image_spec.rb b/spec/lib/api/entities/ci/job_request/image_spec.rb index 55aade03129..3ab14ffc3ae 100644 --- a/spec/lib/api/entities/ci/job_request/image_spec.rb +++ b/spec/lib/api/entities/ci/job_request/image_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe API::Entities::Ci::JobRequest::Image do let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]} - let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)} + let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports, pull_policy: ['if-not-present']) } let(:entity) { described_class.new(image) } subject { entity.as_json } @@ -28,4 +28,18 @@ RSpec.describe API::Entities::Ci::JobRequest::Image do expect(subject[:ports]).to be_nil end end + + it 'returns the pull policy' do + expect(subject[:pull_policy]).to eq(['if-not-present']) + end + + context 'when the FF ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'does not return the pull policy' do + expect(subject).not_to have_key(:pull_policy) + end + end end diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 630dfcd06bb..8f77a1f60ad 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -28,8 +28,14 @@ RSpec.describe Gitlab::Ci::Build::Image do context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } + let(:pull_policy) { %w[always if-not-present] } - let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) } + let(:job) do + create(:ci_build, options: { image: { name: image_name, + entrypoint: entrypoint, + ports: [80], + pull_policy: pull_policy } } ) + end it 'fabricates an object of the proper class' do is_expected.to be_kind_of(described_class) @@ -38,6 +44,7 @@ RSpec.describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper attributes' do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) + expect(subject.pull_policy).to eq(pull_policy) end it 'populates the ports' do diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index e16a9a7a74a..bd1ab5d8c41 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -1,8 +1,16 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' +require 'support/helpers/stubbed_feature' +require 'support/helpers/stub_feature_flags' RSpec.describe Gitlab::Ci::Config::Entry::Image do + include StubFeatureFlags + + before do + stub_feature_flags(ci_docker_image_pull_policy: true) + end + let(:entry) { described_class.new(config) } context 'when configuration is a string' do @@ -43,6 +51,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do expect(entry.ports).to be_nil end end + + describe '#pull_policy' do + it "returns nil" do + expect(entry.pull_policy).to be_nil + end + end end context 'when configuration is a hash' do @@ -109,6 +123,56 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do end end end + + context 'when configuration has pull_policy' do + let(:config) { { name: 'image:1.0', pull_policy: 'if-not-present' } } + + describe '#valid?' do + it 'is valid' do + entry.compose! + + expect(entry).to be_valid + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'is not valid' do + entry.compose! + + expect(entry).not_to be_valid + expect(entry.errors).to include('image config contains unknown keys: pull_policy') + end + end + end + + describe '#value' do + it "returns value" do + entry.compose! + + expect(entry.value).to eq( + name: 'image:1.0', + pull_policy: ['if-not-present'] + ) + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'is not valid' do + entry.compose! + + expect(entry.value).to eq( + name: 'image:1.0' + ) + end + end + end + end end context 'when entry value is not correct' do diff --git a/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb new file mode 100644 index 00000000000..c35355b10c6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy do + let(:entry) { described_class.new(config) } + + describe '#value' do + subject(:value) { entry.value } + + context 'when config value is nil' do + let(:config) { nil } + + it { is_expected.to be_nil } + end + + context 'when retry value is an empty array' do + let(:config) { [] } + + it { is_expected.to eq(nil) } + end + + context 'when retry value is string' do + let(:config) { "always" } + + it { is_expected.to eq(%w[always]) } + end + + context 'when retry value is array' do + let(:config) { %w[always if-not-present] } + + it { is_expected.to eq(%w[always if-not-present]) } + end + end + + describe 'validation' do + subject(:valid?) { entry.valid? } + + context 'when retry value is nil' do + let(:config) { nil } + + it { is_expected.to eq(false) } + end + + context 'when retry value is an empty array' do + let(:config) { [] } + + it { is_expected.to eq(false) } + end + + context 'when retry value is a hash' do + let(:config) { {} } + + it { is_expected.to eq(false) } + end + + context 'when retry value is string' do + let(:config) { "always" } + + it { is_expected.to eq(true) } + + context 'when it is an invalid policy' do + let(:config) { "invalid" } + + it { is_expected.to eq(false) } + end + + context 'when it is an empty string' do + let(:config) { "" } + + it { is_expected.to eq(false) } + end + end + + context 'when retry value is array' do + let(:config) { %w[always if-not-present] } + + it { is_expected.to eq(true) } + + context 'when config contains an invalid policy' do + let(:config) { %w[always invalid] } + + it { is_expected.to eq(false) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 1910057622b..3dd9ca35881 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -7,7 +7,7 @@ module Gitlab RSpec.describe YamlProcessor do include StubRequests - subject { described_class.new(config, user: nil).execute } + subject(:processor) { described_class.new(config, user: nil).execute } shared_examples 'returns errors' do |error_message| it 'adds a message when an error is encountered' do @@ -965,6 +965,51 @@ module Gitlab }) end end + + context 'when image has pull_policy' do + let(:config) do + <<~YAML + image: + name: ruby:2.7 + pull_policy: if-not-present + + test: + script: exit 0 + YAML + end + + it { is_expected.to be_valid } + + it "returns image and service when defined" do + expect(processor.stage_builds_attributes("test")).to contain_exactly({ + stage: "test", + stage_idx: 2, + name: "test", + only: { refs: %w[branches tags] }, + options: { + script: ["exit 0"], + image: { name: "ruby:2.7", pull_policy: ["if-not-present"] } + }, + allow_failure: false, + when: "on_success", + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :stage + }) + end + + context 'when the feature flag ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it { is_expected.not_to be_valid } + + it "returns no job" do + expect(processor.jobs).to eq({}) + end + end + end end describe 'Variables' do diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb index 84bd782c467..aa456736f78 100644 --- a/spec/lib/gitlab/import_export/lfs_saver_spec.rb +++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb @@ -45,6 +45,18 @@ RSpec.describe Gitlab::ImportExport::LfsSaver do expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}") end + context 'when lfs object has file on disk missing' do + it 'does not attempt to copy non-existent file' do + FileUtils.rm(lfs_object.file.path) + expect(saver).not_to receive(:copy_files) + + saver.save # rubocop:disable Rails/SaveBang + + expect(shared.errors).to be_empty + expect(File).not_to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}") + end + end + describe 'saving a json file' do before do # Create two more LfsObjectProject records with different `repository_type`s diff --git a/spec/models/concerns/limitable_spec.rb b/spec/models/concerns/limitable_spec.rb index 850282d54c7..c0a6aea2075 100644 --- a/spec/models/concerns/limitable_spec.rb +++ b/spec/models/concerns/limitable_spec.rb @@ -31,7 +31,7 @@ RSpec.describe Limitable do it 'triggers scoped validations' do instance = MinimalTestClass.new - expect(instance).to receive(:validate_scoped_plan_limit_not_exceeded) + expect(instance).to receive(:scoped_plan_limits) instance.valid?(:create) end @@ -94,7 +94,7 @@ RSpec.describe Limitable do it 'triggers scoped validations' do instance = MinimalTestClass.new - expect(instance).to receive(:validate_global_plan_limit_not_exceeded) + expect(instance).to receive(:global_plan_limits) instance.valid?(:create) end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 5fe3882cee9..fd89a3a2e22 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -587,22 +587,10 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do expect(action.user).to eq(user) end - context 'env_stopped_on_stop_success feature flag' do - it 'environment is not stopped when flag is enabled' do - stub_feature_flags(env_stopped_on_stop_success: true) - - subject - - expect(environment).not_to be_stopped - end - - it 'environment is stopped when flag is disabled' do - stub_feature_flags(env_stopped_on_stop_success: false) - - subject + it 'environment is not stopped' do + subject - expect(environment).to be_stopped - end + expect(environment).not_to be_stopped end end diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb index 381e42978f4..78521e4bdf2 100644 --- a/spec/models/plan_limits_spec.rb +++ b/spec/models/plan_limits_spec.rb @@ -215,6 +215,7 @@ RSpec.describe PlanLimits do web_hook_calls ci_daily_pipeline_schedule_triggers repository_size + security_policy_scan_execution_schedules ] + disabled_max_artifact_size_columns end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 23e4641e0d5..dfb625abc1b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -32,7 +32,7 @@ RSpec.describe ProjectPolicy do end end - it 'does not include the read_issue permission when the issue author is not a member of the private project' do + it 'does not include the read permissions when the issue author is not a member of the private project' do project = create(:project, :private) issue = create(:issue, project: project, author: create(:user)) user = issue.author @@ -40,6 +40,7 @@ RSpec.describe ProjectPolicy do expect(project.team.member?(issue.author)).to be false expect(Ability).not_to be_allowed(user, :read_issue, project) + expect(Ability).not_to be_allowed(user, :read_work_item, project) end it_behaves_like 'model with wiki policies' do @@ -61,7 +62,7 @@ RSpec.describe ProjectPolicy do end it 'does not include the issues permissions' do - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item end it 'disables boards and lists permissions' do @@ -73,7 +74,7 @@ RSpec.describe ProjectPolicy do it 'does not include the issues permissions' do create(:jira_integration, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident, :create_work_item, :create_task, :read_work_item end end end @@ -752,14 +753,14 @@ RSpec.describe ProjectPolicy do allow(project).to receive(:service_desk_enabled?).and_return(true) end - it { expect_allowed(:reporter_access, :create_note, :read_issue) } + it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) } context 'when issues are protected members only' do before do project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) end - it { expect_allowed(:reporter_access, :create_note, :read_issue) } + it { expect_allowed(:reporter_access, :create_note, :read_issue, :read_work_item) } end end end diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb index b19f7d2557d..9cfc4455979 100644 --- a/spec/policies/work_item_policy_spec.rb +++ b/spec/policies/work_item_policy_spec.rb @@ -37,6 +37,12 @@ RSpec.describe WorkItemPolicy do let(:current_user) { guest_author } it { is_expected.to be_allowed(:read_work_item) } + + context 'when work_item is confidential' do + let(:work_item_subject) { create(:work_item, confidential: true, project: project) } + + it { is_expected.not_to be_allowed(:read_work_item) } + end end end diff --git a/spec/requests/api/ci/runner/jobs_request_post_spec.rb b/spec/requests/api/ci/runner/jobs_request_post_spec.rb index dbc5f0e74e2..3c6f9ac2816 100644 --- a/spec/requests/api/ci/runner/jobs_request_post_spec.rb +++ b/spec/requests/api/ci/runner/jobs_request_post_spec.rb @@ -216,7 +216,7 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do expect(json_response['token']).to eq(job.token) expect(json_response['job_info']).to eq(expected_job_info) expect(json_response['git_info']).to eq(expected_git_info) - expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [] }) + expect(json_response['image']).to eq({ 'name' => 'image:1.0', 'entrypoint' => '/bin/sh', 'ports' => [], 'pull_policy' => nil }) expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil, 'alias' => nil, 'command' => nil, 'ports' => [], 'variables' => nil }, { 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh', @@ -810,6 +810,45 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do end end + context 'when image has pull_policy' do + let(:job) { create(:ci_build, :pending, :queued, pipeline: pipeline, options: options) } + + let(:options) do + { + image: { + name: 'ruby', + pull_policy: ['if-not-present'] + } + } + end + + it 'returns the image with pull policy' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => { 'name' => 'ruby', 'pull_policy' => ['if-not-present'], 'entrypoint' => nil, 'ports' => [] } + ) + end + + context 'when the FF ci_docker_image_pull_policy is disabled' do + before do + stub_feature_flags(ci_docker_image_pull_policy: false) + end + + it 'returns the image without pull policy' do + request_job + + expect(response).to have_gitlab_http_status(:created) + expect(json_response).to include( + 'id' => job.id, + 'image' => { 'name' => 'ruby', 'entrypoint' => nil, 'ports' => [] } + ) + end + end + end + describe 'a job with excluded artifacts' do context 'when excluded paths are defined' do let(:job) do diff --git a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb index 5ae54ed309b..894789c7941 100644 --- a/spec/services/bulk_imports/lfs_objects_export_service_spec.rb +++ b/spec/services/bulk_imports/lfs_objects_export_service_spec.rb @@ -53,6 +53,18 @@ RSpec.describe BulkImports::LfsObjectsExportService do ) end + context 'when lfs object has file on disk missing' do + it 'does not attempt to copy non-existent file' do + FileUtils.rm(lfs_object.file.path) + + expect(service).not_to receive(:copy_files) + + service.execute + + expect(File).not_to exist(File.join(export_path, lfs_object.oid)) + end + end + context 'when lfs object is remotely stored' do let(:lfs_object) { create(:lfs_object, :object_storage) } diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb index d9cbea58406..afb3976e3b8 100644 --- a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb @@ -12,7 +12,7 @@ RSpec.shared_context 'IssuesFinder context' do let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) } let_it_be(:label) { create(:label, project: project2) } let_it_be(:label2) { create(:label, project: project2) } - let_it_be(:issue1, reload: true) do + let_it_be(:item1, reload: true) do create(:issue, author: user, assignees: [user], @@ -23,7 +23,7 @@ RSpec.shared_context 'IssuesFinder context' do updated_at: 1.week.ago) end - let_it_be(:issue2, reload: true) do + let_it_be(:item2, reload: true) do create(:issue, author: user, assignees: [user], @@ -33,7 +33,7 @@ RSpec.shared_context 'IssuesFinder context' do updated_at: 1.week.from_now) end - let_it_be(:issue3, reload: true) do + let_it_be(:item3, reload: true) do create(:issue, author: user2, assignees: [user2], @@ -44,8 +44,8 @@ RSpec.shared_context 'IssuesFinder context' do updated_at: 2.weeks.from_now) end - let_it_be(:issue4, reload: true) { create(:issue, project: project3) } - let_it_be(:issue5, reload: true) do + let_it_be(:item4, reload: true) { create(:issue, project: project3) } + let_it_be(:item5, reload: true) do create(:issue, author: user, assignees: [user], @@ -55,18 +55,20 @@ RSpec.shared_context 'IssuesFinder context' do updated_at: 3.days.ago) end - let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } - let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } - let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } + let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) } + let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) } + let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) } + + let(:items_model) { Issue } end RSpec.shared_context 'IssuesFinder#execute context' do - let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - let!(:label_link2) { create(:label_link, label: label2, target: issue3) } + let!(:closed_item) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: item2) } + let!(:label_link2) { create(:label_link, label: label2, target: item3) } let(:search_user) { user } let(:params) { {} } - let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } before_all do project1.add_maintainer(user) diff --git a/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb new file mode 100644 index 00000000000..8c5bc339db5 --- /dev/null +++ b/spec/support/shared_contexts/finders/work_items_finder_shared_contexts.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +RSpec.shared_context 'WorkItemsFinder context' do + let_it_be(:user) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project1, reload: true) { create(:project, group: group) } + let_it_be(:project2, reload: true) { create(:project) } + let_it_be(:project3, reload: true) { create(:project, group: subgroup) } + let_it_be(:release) { create(:release, project: project1, tag: 'v1.0.0') } + let_it_be(:milestone) { create(:milestone, project: project1, releases: [release]) } + let_it_be(:label) { create(:label, project: project2) } + let_it_be(:label2) { create(:label, project: project2) } + let_it_be(:item1, reload: true) do + create(:work_item, + author: user, + assignees: [user], + project: project1, + milestone: milestone, + title: 'gitlab', + created_at: 1.week.ago, + updated_at: 1.week.ago) + end + + let_it_be(:item2, reload: true) do + create(:work_item, + author: user, + assignees: [user], + project: project2, + description: 'gitlab', + created_at: 1.week.from_now, + updated_at: 1.week.from_now) + end + + let_it_be(:item3, reload: true) do + create(:work_item, + author: user2, + assignees: [user2], + project: project2, + title: 'tanuki', + description: 'tanuki', + created_at: 2.weeks.from_now, + updated_at: 2.weeks.from_now) + end + + let_it_be(:item4, reload: true) { create(:work_item, project: project3) } + let_it_be(:item5, reload: true) do + create(:work_item, + author: user, + assignees: [user], + project: project1, + title: 'wotnot', + created_at: 3.days.ago, + updated_at: 3.days.ago) + end + + let_it_be(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: item1) } + let_it_be(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: item2) } + let_it_be(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: item3) } + + let(:items_model) { WorkItem } +end + +RSpec.shared_context 'WorkItemsFinder#execute context' do + let!(:closed_item) { create(:work_item, author: user2, assignees: [user2], project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: item2) } + let!(:label_link2) { create(:label_link, label: label2, target: item3) } + let(:search_user) { user } + let(:params) { {} } + let(:items) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + + before_all do + project1.add_maintainer(user) + project2.add_developer(user) + project2.add_developer(user2) + project3.add_developer(user) + end +end diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb new file mode 100644 index 00000000000..622a88e8323 --- /dev/null +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -0,0 +1,1471 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples 'issues or work items finder' do |factory, execute_context| + describe '#execute' do + include_context execute_context + + context 'scope: all' do + let(:scope) { 'all' } + + it 'returns all items' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5) + end + + context 'user does not have read permissions' do + let(:search_user) { user2 } + + context 'when filtering by project id' do + let(:params) { { project_id: project1.id } } + + it 'returns no items' do + expect(items).to be_empty + end + end + + context 'when filtering by group id' do + let(:params) { { group_id: group.id } } + + it 'returns no items' do + expect(items).to be_empty + end + end + end + + context 'assignee filtering' do + let(:issuables) { items } + + it_behaves_like 'assignee ID filter' do + let(:params) { { assignee_id: user.id } } + let(:expected_issuables) { [item1, item2, item5] } + end + + it_behaves_like 'assignee NOT ID filter' do + let(:params) { { not: { assignee_id: user.id } } } + let(:expected_issuables) { [item3, item4] } + end + + it_behaves_like 'assignee OR filter' do + let(:params) { { or: { assignee_id: [user.id, user2.id] } } } + let(:expected_issuables) { [item1, item2, item3, item5] } + end + + context 'when assignee_id does not exist' do + it_behaves_like 'assignee NOT ID filter' do + let(:params) { { not: { assignee_id: -100 } } } + let(:expected_issuables) { [item1, item2, item3, item4, item5] } + end + end + + context 'filter by username' do + let_it_be(:user3) { create(:user) } + + before do + project2.add_developer(user3) + item2.assignees = [user2] + item3.assignees = [user3] + end + + it_behaves_like 'assignee username filter' do + let(:params) { { assignee_username: [user2.username] } } + let(:expected_issuables) { [item2] } + end + + it_behaves_like 'assignee NOT username filter' do + before do + item2.assignees = [user2] + end + + let(:params) { { not: { assignee_username: [user.username, user2.username] } } } + let(:expected_issuables) { [item3, item4] } + end + + it_behaves_like 'assignee OR filter' do + let(:params) { { or: { assignee_username: [user2.username, user3.username] } } } + let(:expected_issuables) { [item2, item3] } + end + + context 'when assignee_username does not exist' do + it_behaves_like 'assignee NOT username filter' do + before do + item2.assignees = [user2] + end + + let(:params) { { not: { assignee_username: 'non_existent_username' } } } + let(:expected_issuables) { [item1, item2, item3, item4, item5] } + end + end + end + + it_behaves_like 'no assignee filter' do + let_it_be(:user3) { create(:user) } + let(:expected_issuables) { [item4] } + end + + it_behaves_like 'any assignee filter' do + let(:expected_issuables) { [item1, item2, item3, item5] } + end + end + + context 'filtering by release' do + context 'when the release tag is none' do + let(:params) { { release_tag: 'none' } } + + it 'returns items without releases' do + expect(items).to contain_exactly(item2, item3, item4, item5) + end + end + + context 'when the release tag exists' do + let(:params) { { project_id: project1.id, release_tag: release.tag } } + + it 'returns the items associated with that release' do + expect(items).to contain_exactly(item1) + end + end + end + + context 'filtering by projects' do + context 'when projects are passed in a list of ids' do + let(:params) { { projects: [project1.id] } } + + it 'returns the item belonging to the projects' do + expect(items).to contain_exactly(item1, item5) + end + end + + context 'when projects are passed in a subquery' do + let(:params) { { projects: Project.id_in(project1.id) } } + + it 'returns the item belonging to the projects' do + expect(items).to contain_exactly(item1, item5) + end + end + end + + context 'filtering by group_id' do + let(:params) { { group_id: group.id } } + + context 'when include_subgroup param not set' do + it 'returns all group items' do + expect(items).to contain_exactly(item1, item5) + end + + context 'when projects outside the group are passed' do + let(:params) { { group_id: group.id, projects: [project2.id] } } + + it 'returns no items' do + expect(items).to be_empty + end + end + + context 'when projects of the group are passed' do + let(:params) { { group_id: group.id, projects: [project1.id] } } + + it 'returns the item within the group and projects' do + expect(items).to contain_exactly(item1, item5) + end + end + + context 'when projects of the group are passed as a subquery' do + let(:params) { { group_id: group.id, projects: Project.id_in(project1.id) } } + + it 'returns the item within the group and projects' do + expect(items).to contain_exactly(item1, item5) + end + end + + context 'when release_tag is passed as a parameter' do + let(:params) { { group_id: group.id, release_tag: 'dne-release-tag' } } + + it 'ignores the release_tag parameter' do + expect(items).to contain_exactly(item1, item5) + end + end + end + + context 'when include_subgroup param is true' do + before do + params[:include_subgroups] = true + end + + it 'returns all group and subgroup items' do + expect(items).to contain_exactly(item1, item4, item5) + end + + context 'when mixed projects are passed' do + let(:params) { { group_id: group.id, projects: [project2.id, project3.id] } } + + it 'returns the item within the group and projects' do + expect(items).to contain_exactly(item4) + end + end + end + end + + context 'filtering by author' do + context 'by author ID' do + let(:params) { { author_id: user2.id } } + + it 'returns items created by that user' do + expect(items).to contain_exactly(item3) + end + end + + context 'using OR' do + let(:item6) { create(factory, project: project2) } + let(:params) { { or: { author_username: [item3.author.username, item6.author.username] } } } + + it 'returns items created by any of the given users' do + expect(items).to contain_exactly(item3, item6) + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(or_issuable_queries: false) + end + + it 'does not add any filter' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5, item6) + end + end + end + + context 'filtering by NOT author ID' do + let(:params) { { not: { author_id: user2.id } } } + + it 'returns items not created by that user' do + expect(items).to contain_exactly(item1, item2, item4, item5) + end + end + + context 'filtering by nonexistent author ID and issue term using CTE for search' do + let(:params) do + { + author_id: 'does-not-exist', + search: 'git', + attempt_group_search_optimizations: true + } + end + + it 'returns no results' do + expect(items).to be_empty + end + end + end + + context 'filtering by milestone' do + let(:params) { { milestone_title: milestone.title } } + + it 'returns items assigned to that milestone' do + expect(items).to contain_exactly(item1) + end + end + + context 'filtering by not milestone' do + let(:params) { { not: { milestone_title: milestone.title } } } + + it 'returns items not assigned to that milestone' do + expect(items).to contain_exactly(item2, item3, item4, item5) + end + end + + context 'filtering by group milestone' do + let!(:group) { create(:group, :public) } + let(:group_milestone) { create(:milestone, group: group) } + let!(:group_member) { create(:group_member, group: group, user: user) } + let(:params) { { milestone_title: group_milestone.title } } + + before do + project2.update!(namespace: group) + item2.update!(milestone: group_milestone) + item3.update!(milestone: group_milestone) + end + + it 'returns items assigned to that group milestone' do + expect(items).to contain_exactly(item2, item3) + end + + context 'using NOT' do + let(:params) { { not: { milestone_title: group_milestone.title } } } + + it 'returns items not assigned to that group milestone' do + expect(items).to contain_exactly(item1, item4, item5) + end + end + end + + context 'filtering by no milestone' do + let(:params) { { milestone_title: 'None' } } + + it 'returns items with no milestone' do + expect(items).to contain_exactly(item2, item3, item4, item5) + end + + it 'returns items with no milestone (deprecated)' do + params[:milestone_title] = Milestone::None.title + + expect(items).to contain_exactly(item2, item3, item4, item5) + end + end + + context 'filtering by any milestone' do + let(:params) { { milestone_title: 'Any' } } + + it 'returns items with any assigned milestone' do + expect(items).to contain_exactly(item1) + end + + it 'returns items with any assigned milestone (deprecated)' do + params[:milestone_title] = Milestone::Any.title + + expect(items).to contain_exactly(item1) + end + end + + context 'filtering by upcoming milestone' do + let(:params) { { milestone_title: Milestone::Upcoming.name } } + + let!(:group) { create(:group, :public) } + let!(:group_member) { create(:group_member, group: group, user: user) } + + let(:project_no_upcoming_milestones) { create(:project, :public) } + let(:project_next_1_1) { create(:project, :public) } + let(:project_next_8_8) { create(:project, :public) } + let(:project_in_group) { create(:project, :public, namespace: group) } + + let(:yesterday) { Date.current - 1.day } + let(:tomorrow) { Date.current + 1.day } + let(:two_days_from_now) { Date.current + 2.days } + let(:ten_days_from_now) { Date.current + 10.days } + + let(:milestones) do + [ + create(:milestone, :closed, project: project_no_upcoming_milestones), + create(:milestone, project: project_next_1_1, title: '1.1', due_date: two_days_from_now), + create(:milestone, project: project_next_1_1, title: '8.9', due_date: ten_days_from_now), + create(:milestone, project: project_next_8_8, title: '1.2', due_date: yesterday), + create(:milestone, project: project_next_8_8, title: '8.8', due_date: tomorrow), + create(:milestone, group: group, title: '9.9', due_date: tomorrow) + ] + end + + let!(:created_items) do + milestones.map do |milestone| + create(factory, project: milestone.project || project_in_group, + milestone: milestone, author: user, assignees: [user]) + end + end + + it 'returns items in the upcoming milestone for each project or group' do + expect(items.map { |item| item.milestone.title }) + .to contain_exactly('1.1', '8.8', '9.9') + expect(items.map { |item| item.milestone.due_date }) + .to contain_exactly(tomorrow, two_days_from_now, tomorrow) + end + + context 'using NOT' do + let(:params) { { not: { milestone_title: Milestone::Upcoming.name } } } + + it 'returns items not in upcoming milestones for each project or group, but must have a due date' do + target_items = created_items.select do |item| + item.milestone&.due_date && item.milestone.due_date <= Date.current + end + + expect(items).to contain_exactly(*target_items) + end + end + end + + context 'filtering by started milestone' do + let(:params) { { milestone_title: Milestone::Started.name } } + + let(:project_no_started_milestones) { create(:project, :public) } + let(:project_started_1_and_2) { create(:project, :public) } + let(:project_started_8) { create(:project, :public) } + + let(:yesterday) { Date.current - 1.day } + let(:tomorrow) { Date.current + 1.day } + let(:two_days_ago) { Date.current - 2.days } + let(:three_days_ago) { Date.current - 3.days } + + let(:milestones) do + [ + create(:milestone, project: project_no_started_milestones, start_date: tomorrow), + create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago), + create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday), + create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow), + create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago), + create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago), + create(:milestone, project: project_started_8, title: '7.0'), + create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday), + create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow) + ] + end + + before do + milestones.each do |milestone| + create(factory, project: milestone.project, milestone: milestone, author: user, assignees: [user]) + end + end + + it 'returns items in the started milestones for each project' do + expect(items.map { |item| item.milestone.title }) + .to contain_exactly('1.0', '2.0', '8.0') + expect(items.map { |item| item.milestone.start_date }) + .to contain_exactly(two_days_ago, yesterday, yesterday) + end + + context 'using NOT' do + let(:params) { { not: { milestone_title: Milestone::Started.name } } } + + it 'returns items not in the started milestones for each project' do + target_items = items_model.where(milestone: Milestone.not_started) + + expect(items).to contain_exactly(*target_items) + end + end + end + + context 'filtering by label' do + let(:params) { { label_name: label.title } } + + it 'returns items with that label' do + expect(items).to contain_exactly(item2) + end + + context 'using NOT' do + let(:params) { { not: { label_name: label.title } } } + + it 'returns items that do not have that label' do + expect(items).to contain_exactly(item1, item3, item4, item5) + end + + # IssuableFinder first filters using the outer params (the ones not inside the `not` key.) + # Afterwards, it applies the `not` params to that resultset. This means that things inside the `not` param + # do not take precedence over the outer params with the same name. + context 'shadowing the same outside param' do + let(:params) { { label_name: label2.title, not: { label_name: label.title } } } + + it 'does not take precedence over labels outside NOT' do + expect(items).to contain_exactly(item3) + end + end + + context 'further filtering outside params' do + let(:params) { { label_name: label2.title, not: { assignee_username: user2.username } } } + + it 'further filters on the returned resultset' do + expect(items).to be_empty + end + end + end + end + + context 'filtering by multiple labels' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label2) { create(:label, project: project2) } + + before do + create(:label_link, label: label2, target: item2) + end + + it 'returns the unique items with all those labels' do + expect(items).to contain_exactly(item2) + end + + context 'using NOT' do + let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } + + it 'returns items that do not have any of the labels provided' do + expect(items).to contain_exactly(item1, item4, item5) + end + end + end + + context 'filtering by a label that includes any or none in the title' do + let(:params) { { label_name: [label.title, label2.title].join(',') } } + let(:label) { create(:label, title: 'any foo', project: project2) } + let(:label2) { create(:label, title: 'bar none', project: project2) } + + before do + create(:label_link, label: label2, target: item2) + end + + it 'returns the unique items with all those labels' do + expect(items).to contain_exactly(item2) + end + + context 'using NOT' do + let(:params) { { not: { label_name: [label.title, label2.title].join(',') } } } + + it 'returns items that do not have ANY ONE of the labels provided' do + expect(items).to contain_exactly(item1, item4, item5) + end + end + end + + context 'filtering by no label' do + let(:params) { { label_name: described_class::Params::FILTER_NONE } } + + it 'returns items with no labels' do + expect(items).to contain_exactly(item1, item4, item5) + end + end + + context 'filtering by any label' do + let(:params) { { label_name: described_class::Params::FILTER_ANY } } + + it 'returns items that have one or more label' do + create_list(:label_link, 2, label: create(:label, project: project2), target: item3) + + expect(items).to contain_exactly(item2, item3) + end + end + + context 'when the same label exists on project and group levels' do + let(:item1) { create(factory, project: project1) } + let(:item2) { create(factory, project: project1) } + + # Skipping validation to reproduce a "real-word" scenario. + # We still have legacy labels on PRD that have the same title on the group and project levels, example: `bug` + let(:project_label) do + build(:label, title: 'somelabel', project: project1).tap { |r| r.save!(validate: false) } + end + + let(:group_label) { create(:group_label, title: 'somelabel', group: project1.group) } + + let(:params) { { label_name: 'somelabel' } } + + before do + create(:label_link, label: group_label, target: item1) + create(:label_link, label: project_label, target: item2) + end + + it 'finds both item records' do + expect(items).to contain_exactly(item1, item2) + end + end + + context 'filtering by item term' do + let(:params) { { search: search_term } } + + let_it_be(:english) { create(factory, project: project1, title: 'title', description: 'something english') } + + let_it_be(:japanese) do + create(factory, project: project1, title: '日本語 title', description: 'another english description') + end + + context 'with latin search term' do + let(:search_term) { 'title english' } + + it 'returns matching items' do + expect(items).to contain_exactly(english, japanese) + end + end + + context 'with non-latin search term' do + let(:search_term) { '日本語' } + + it 'returns matching items' do + expect(items).to contain_exactly(japanese) + end + end + + context 'when full-text search is disabled' do + let(:search_term) { 'somet' } + + before do + stub_feature_flags(issues_full_text_search: false) + end + + it 'allows partial word matches' do + expect(items).to contain_exactly(english) + end + end + + context 'with anonymous user' do + let_it_be(:public_project) { create(:project, :public, group: subgroup) } + let_it_be(:item6) { create(factory, project: public_project, title: 'tanuki') } + let_it_be(:item7) { create(factory, project: public_project, title: 'ikunat') } + + let(:search_user) { nil } + let(:params) { { search: 'tanuki' } } + + context 'with disable_anonymous_search feature flag enabled' do + before do + stub_feature_flags(disable_anonymous_search: true) + end + + it 'does not perform search' do + expect(items).to contain_exactly(item6, item7) + end + end + + context 'with disable_anonymous_search feature flag disabled' do + before do + stub_feature_flags(disable_anonymous_search: false) + end + + it 'finds one public item' do + expect(items).to contain_exactly(item6) + end + end + end + end + + context 'filtering by item term in title' do + let(:params) { { search: 'git', in: 'title' } } + + it 'returns items with title match for search term' do + expect(items).to contain_exactly(item1) + end + end + + context 'filtering by items iids' do + let(:params) { { iids: [item3.iid] } } + + it 'returns items where iids match' do + expect(items).to contain_exactly(item3, item5) + end + + context 'using NOT' do + let(:params) { { not: { iids: [item3.iid] } } } + + it 'returns items with no iids match' do + expect(items).to contain_exactly(item1, item2, item4) + end + end + end + + context 'filtering by state' do + context 'with opened' do + let(:params) { { state: 'opened' } } + + it 'returns only opened items' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5) + end + end + + context 'with closed' do + let(:params) { { state: 'closed' } } + + it 'returns only closed items' do + expect(items).to contain_exactly(closed_item) + end + end + + context 'with all' do + let(:params) { { state: 'all' } } + + it 'returns all items' do + expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5) + end + end + + context 'with invalid state' do + let(:params) { { state: 'invalid_state' } } + + it 'returns all items' do + expect(items).to contain_exactly(item1, item2, item3, closed_item, item4, item5) + end + end + end + + context 'filtering by created_at' do + context 'through created_after' do + let(:params) { { created_after: item3.created_at } } + + it 'returns items created on or after the given date' do + expect(items).to contain_exactly(item3) + end + end + + context 'through created_before' do + let(:params) { { created_before: item1.created_at } } + + it 'returns items created on or before the given date' do + expect(items).to contain_exactly(item1) + end + end + + context 'through created_after and created_before' do + let(:params) { { created_after: item2.created_at, created_before: item3.created_at } } + + it 'returns items created between the given dates' do + expect(items).to contain_exactly(item2, item3) + end + end + end + + context 'filtering by updated_at' do + context 'through updated_after' do + let(:params) { { updated_after: item3.updated_at } } + + it 'returns items updated on or after the given date' do + expect(items).to contain_exactly(item3) + end + end + + context 'through updated_before' do + let(:params) { { updated_before: item1.updated_at } } + + it 'returns items updated on or before the given date' do + expect(items).to contain_exactly(item1) + end + end + + context 'through updated_after and updated_before' do + let(:params) { { updated_after: item2.updated_at, updated_before: item3.updated_at } } + + it 'returns items updated between the given dates' do + expect(items).to contain_exactly(item2, item3) + end + end + end + + context 'filtering by closed_at' do + let!(:closed_item1) { create(factory, project: project1, state: :closed, closed_at: 1.week.ago) } + let!(:closed_item2) { create(factory, project: project2, state: :closed, closed_at: 1.week.from_now) } + let!(:closed_item3) { create(factory, project: project2, state: :closed, closed_at: 2.weeks.from_now) } + + context 'through closed_after' do + let(:params) { { state: :closed, closed_after: closed_item3.closed_at } } + + it 'returns items closed on or after the given date' do + expect(items).to contain_exactly(closed_item3) + end + end + + context 'through closed_before' do + let(:params) { { state: :closed, closed_before: closed_item1.closed_at } } + + it 'returns items closed on or before the given date' do + expect(items).to contain_exactly(closed_item1) + end + end + + context 'through closed_after and closed_before' do + let(:params) do + { state: :closed, closed_after: closed_item2.closed_at, closed_before: closed_item3.closed_at } + end + + it 'returns items closed between the given dates' do + expect(items).to contain_exactly(closed_item2, closed_item3) + end + end + end + + context 'filtering by reaction name' do + context 'user searches by no reaction' do + let(:params) { { my_reaction_emoji: 'None' } } + + it 'returns items that the user did not react to' do + expect(items).to contain_exactly(item2, item4, item5) + end + end + + context 'user searches by any reaction' do + let(:params) { { my_reaction_emoji: 'Any' } } + + it 'returns items that the user reacted to' do + expect(items).to contain_exactly(item1, item3) + end + end + + context 'user searches by "thumbsup" reaction' do + let(:params) { { my_reaction_emoji: 'thumbsup' } } + + it 'returns items that the user thumbsup to' do + expect(items).to contain_exactly(item1) + end + + context 'using NOT' do + let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } } + + it 'returns items that the user did not thumbsup to' do + expect(items).to contain_exactly(item2, item3, item4, item5) + end + end + end + + context 'user2 searches by "thumbsup" reaction' do + let(:search_user) { user2 } + + let(:params) { { my_reaction_emoji: 'thumbsup' } } + + it 'returns items that the user2 thumbsup to' do + expect(items).to contain_exactly(item2) + end + + context 'using NOT' do + let(:params) { { not: { my_reaction_emoji: 'thumbsup' } } } + + it 'returns items that the user2 thumbsup to' do + expect(items).to contain_exactly(item3) + end + end + end + + context 'user searches by "thumbsdown" reaction' do + let(:params) { { my_reaction_emoji: 'thumbsdown' } } + + it 'returns items that the user thumbsdown to' do + expect(items).to contain_exactly(item3) + end + + context 'using NOT' do + let(:params) { { not: { my_reaction_emoji: 'thumbsdown' } } } + + it 'returns items that the user thumbsdown to' do + expect(items).to contain_exactly(item1, item2, item4, item5) + end + end + end + end + + context 'filtering by confidential' do + let_it_be(:confidential_item) { create(factory, project: project1, confidential: true) } + + context 'no filtering' do + it 'returns all items' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5, confidential_item) + end + end + + context 'user filters confidential items' do + let(:params) { { confidential: true } } + + it 'returns only confidential items' do + expect(items).to contain_exactly(confidential_item) + end + end + + context 'user filters only public items' do + let(:params) { { confidential: false } } + + it 'returns only public items' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5) + end + end + end + + context 'filtering by item type' do + let_it_be(:incident_item) { create(factory, issue_type: :incident, project: project1) } + + context 'no type given' do + let(:params) { { issue_types: [] } } + + it 'returns all items' do + expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5) + end + end + + context 'incident type' do + let(:params) { { issue_types: ['incident'] } } + + it 'returns incident items' do + expect(items).to contain_exactly(incident_item) + end + end + + context 'item type' do + let(:params) { { issue_types: ['issue'] } } + + it 'returns all items with type issue' do + expect(items).to contain_exactly(item1, item2, item3, item4, item5) + end + end + + context 'multiple params' do + let(:params) { { issue_types: %w(issue incident) } } + + it 'returns all items' do + expect(items).to contain_exactly(incident_item, item1, item2, item3, item4, item5) + end + end + + context 'without array' do + let(:params) { { issue_types: 'incident' } } + + it 'returns incident items' do + expect(items).to contain_exactly(incident_item) + end + end + + context 'invalid params' do + let(:params) { { issue_types: ['nonsense'] } } + + it 'returns no items' do + expect(items).to eq(items_model.none) + end + end + end + + context 'filtering by crm contact' do + let_it_be(:contact1) { create(:contact, group: group) } + let_it_be(:contact2) { create(:contact, group: group) } + + let_it_be(:contact1_item1) { create(factory, project: project1) } + let_it_be(:contact1_item2) { create(factory, project: project1) } + let_it_be(:contact2_item1) { create(factory, project: project1) } + + let(:params) { { crm_contact_id: contact1.id } } + + it 'returns for that contact' do + create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1) + create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1) + create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2) + + expect(items).to contain_exactly(contact1_item1, contact1_item2) + end + end + + context 'filtering by crm organization' do + let_it_be(:organization) { create(:organization, group: group) } + let_it_be(:contact1) { create(:contact, group: group, organization: organization) } + let_it_be(:contact2) { create(:contact, group: group, organization: organization) } + + let_it_be(:contact1_item1) { create(factory, project: project1) } + let_it_be(:contact1_item2) { create(factory, project: project1) } + let_it_be(:contact2_item1) { create(factory, project: project1) } + + let(:params) { { crm_organization_id: organization.id } } + + it 'returns for that contact' do + create(:issue_customer_relations_contact, issue: contact1_item1, contact: contact1) + create(:issue_customer_relations_contact, issue: contact1_item2, contact: contact1) + create(:issue_customer_relations_contact, issue: contact2_item1, contact: contact2) + + expect(items).to contain_exactly(contact1_item1, contact1_item2, contact2_item1) + end + end + + context 'when the user is unauthorized' do + let(:search_user) { nil } + + it 'returns no results' do + expect(items).to be_empty + end + end + + context 'when the user can see some, but not all, items' do + let(:search_user) { user2 } + + it 'returns only items they can see' do + expect(items).to contain_exactly(item2, item3) + end + end + + it 'finds items user can access due to group' do + group = create(:group) + project = create(:project, group: group) + item = create(factory, project: project) + group.add_user(user, :owner) + + expect(items).to include(item) + end + end + + context 'personal scope' do + let(:scope) { 'assigned_to_me' } + + it 'returns item assigned to the user' do + expect(items).to contain_exactly(item1, item2, item5) + end + + context 'filtering by project' do + let(:params) { { project_id: project1.id } } + + it 'returns items assigned to the user in that project' do + expect(items).to contain_exactly(item1, item5) + end + end + end + + context 'when project restricts items' do + let(:scope) { nil } + + it "doesn't return team-only items to non team members" do + project = create(:project, :public, :issues_private) + item = create(factory, project: project) + + expect(items).not_to include(item) + end + + it "doesn't return items if feature disabled" do + [project1, project2, project3].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(items.count).to eq 0 + end + end + + context 'external authorization' do + it_behaves_like 'a finder with external authorization service' do + let!(:subject) { create(factory, project: project) } + let(:project_params) { { project_id: project.id } } + end + end + + context 'filtering by due date' do + let_it_be(:item_due_today) { create(factory, project: project1, due_date: Date.current) } + let_it_be(:item_due_tomorrow) { create(factory, project: project1, due_date: 1.day.from_now) } + let_it_be(:item_overdue) { create(factory, project: project1, due_date: 2.days.ago) } + let_it_be(:item_due_soon) { create(factory, project: project1, due_date: 2.days.from_now) } + + let(:scope) { 'all' } + let(:base_params) { { project_id: project1.id } } + + context 'with param set to no due date' do + let(:params) { base_params.merge(due_date: items_model::NoDueDate.name) } + + it 'returns items with no due date' do + expect(items).to contain_exactly(item1, item5) + end + end + + context 'with param set to any due date' do + let(:params) { base_params.merge(due_date: items_model::AnyDueDate.name) } + + it 'returns items with any due date' do + expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon) + end + end + + context 'with param set to due today' do + let(:params) { base_params.merge(due_date: items_model::DueToday.name) } + + it 'returns items due today' do + expect(items).to contain_exactly(item_due_today) + end + end + + context 'with param set to due tomorrow' do + let(:params) { base_params.merge(due_date: items_model::DueTomorrow.name) } + + it 'returns items due today' do + expect(items).to contain_exactly(item_due_tomorrow) + end + end + + context 'with param set to overdue' do + let(:params) { base_params.merge(due_date: items_model::Overdue.name) } + + it 'returns overdue items' do + expect(items).to contain_exactly(item_overdue) + end + end + + context 'with param set to next month and previous two weeks' do + let(:params) { base_params.merge(due_date: items_model::DueNextMonthAndPreviousTwoWeeks.name) } + + it 'returns items due in the previous two weeks and next month' do + expect(items).to contain_exactly(item_due_today, item_due_tomorrow, item_overdue, item_due_soon) + end + end + + context 'with invalid param' do + let(:params) { base_params.merge(due_date: 'foo') } + + it 'returns no items' do + expect(items).to be_empty + end + end + end + end + + describe '#row_count', :request_store do + let_it_be(:admin) { create(:admin) } + + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns the number of rows for the default state' do + finder = described_class.new(admin) + + expect(finder.row_count).to eq(5) + end + + it 'returns the number of rows for a given state' do + finder = described_class.new(admin, state: 'closed') + + expect(finder.row_count).to be_zero + end + end + + context 'when admin mode is disabled' do + it 'returns no rows' do + finder = described_class.new(admin) + + expect(finder.row_count).to be_zero + end + end + + it 'returns -1 if the query times out' do + finder = described_class.new(admin) + + expect_next_instance_of(described_class) do |subfinder| + expect(subfinder).to receive(:execute).and_raise(ActiveRecord::QueryCanceled) + end + + expect(finder.row_count).to eq(-1) + end + end + + describe '#with_confidentiality_access_check' do + let(:guest) { create(:user) } + + let_it_be(:authorized_user) { create(:user) } + let_it_be(:banned_user) { create(:user, :banned) } + let_it_be(:project) { create(:project, namespace: authorized_user.namespace) } + let_it_be(:public_item) { create(factory, project: project) } + let_it_be(:confidential_item) { create(factory, project: project, confidential: true) } + let_it_be(:hidden_item) { create(factory, project: project, author: banned_user) } + + shared_examples 'returns public, does not return hidden or confidential' do + it 'returns only public items' do + expect(subject).to include(public_item) + expect(subject).not_to include(confidential_item, hidden_item) + end + end + + shared_examples 'returns public and confidential, does not return hidden' do + it 'returns only public and confidential items' do + expect(subject).to include(public_item, confidential_item) + expect(subject).not_to include(hidden_item) + end + end + + shared_examples 'returns public and hidden, does not return confidential' do + it 'returns only public and hidden items' do + expect(subject).to include(public_item, hidden_item) + expect(subject).not_to include(confidential_item) + end + end + + shared_examples 'returns public, confidential, and hidden' do + it 'returns all items' do + expect(subject).to include(public_item, confidential_item, hidden_item) + end + end + + context 'when no project filter is given' do + let(:params) { {} } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).with_confidentiality_access_check } + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).with_confidentiality_access_check } + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).with_confidentiality_access_check } + + before do + project.add_guest(guest) + end + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + end + + context 'for a project member with access to view confidential items' do + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } + + it_behaves_like 'returns public and confidential, does not return hidden' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public, confidential, and hidden' + end + end + + context 'for an admin' do + let(:admin_user) { create(:user, :admin) } + + subject { described_class.new(admin_user, params).with_confidentiality_access_check } + + context 'when admin mode is enabled', :enable_admin_mode do + it_behaves_like 'returns public, confidential, and hidden' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public, confidential, and hidden' + end + end + + context 'when admin mode is disabled' do + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + end + end + end + + context 'when searching within a specific project' do + let(:params) { { project_id: project.id } } + + context 'for an anonymous user' do + subject { described_class.new(nil, params).with_confidentiality_access_check } + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + + it 'does not filter by confidentiality' do + expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything) + subject + end + end + + context 'for a user without project membership' do + subject { described_class.new(user, params).with_confidentiality_access_check } + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + + it 'filters by confidentiality' do + expect(subject.to_sql).to match("issues.confidential") + end + end + + context 'for a guest user' do + subject { described_class.new(guest, params).with_confidentiality_access_check } + + before do + project.add_guest(guest) + end + + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + + it 'filters by confidentiality' do + expect(subject.to_sql).to match("issues.confidential") + end + end + + context 'for a project member with access to view confidential items' do + subject { described_class.new(authorized_user, params).with_confidentiality_access_check } + + it_behaves_like 'returns public and confidential, does not return hidden' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public, confidential, and hidden' + end + + it 'does not filter by confidentiality' do + expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'for an admin' do + let(:admin_user) { create(:user, :admin) } + + subject { described_class.new(admin_user, params).with_confidentiality_access_check } + + context 'when admin mode is enabled', :enable_admin_mode do + it_behaves_like 'returns public, confidential, and hidden' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public, confidential, and hidden' + end + + it 'does not filter by confidentiality' do + expect(items_model).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end + + context 'when admin mode is disabled' do + it_behaves_like 'returns public, does not return hidden or confidential' + + context 'when feature flag is disabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it_behaves_like 'returns public and hidden, does not return confidential' + end + + it 'filters by confidentiality' do + expect(subject.to_sql).to match("issues.confidential") + end + end + end + end + end + + describe '#use_cte_for_search?' do + let(:finder) { described_class.new(nil, params) } + + context 'when there is no search param' do + let(:params) { { attempt_group_search_optimizations: true } } + + it 'returns false' do + expect(finder.use_cte_for_search?).to be_falsey + end + end + + context 'when the force_cte param is falsey' do + let(:params) { { search: '日本語' } } + + it 'returns false' do + expect(finder.use_cte_for_search?).to be_falsey + end + end + + context 'when a non-simple sort is given' do + let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'popularity' } } + + it 'returns false' do + expect(finder.use_cte_for_search?).to be_falsey + end + end + + context 'when all conditions are met' do + context "uses group search optimization" do + let(:params) { { search: '日本語', attempt_group_search_optimizations: true } } + + it 'returns true' do + expect(finder.use_cte_for_search?).to be_truthy + expect(finder.execute.to_sql) + .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + end + end + + context "uses project search optimization" do + let(:params) { { search: '日本語', attempt_project_search_optimizations: true } } + + it 'returns true' do + expect(finder.use_cte_for_search?).to be_truthy + expect(finder.execute.to_sql) + .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + end + end + + context 'with simple sort' do + let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: 'updated_desc' } } + + it 'returns true' do + expect(finder.use_cte_for_search?).to be_truthy + expect(finder.execute.to_sql) + .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + end + end + + context 'with simple sort as a symbol' do + let(:params) { { search: '日本語', attempt_project_search_optimizations: true, sort: :updated_desc } } + + it 'returns true' do + expect(finder.use_cte_for_search?).to be_truthy + expect(finder.execute.to_sql) + .to match(/^WITH "issues" AS #{Gitlab::Database::AsWithMaterialized.materialized_if_supported}/) + end + end + end + end + + describe '#parent_param=' do + using RSpec::Parameterized::TableSyntax + + let(:finder) { described_class.new(nil) } + + subject { finder.parent_param = obj } + + where(:klass, :param) do + :Project | :project_id + :Group | :group_id + end + + with_them do + let(:obj) { Object.const_get(klass, false).new } + + it 'sets the params' do + subject + + expect(finder.params[param]).to eq(obj) + end + end + + context 'unexpected parent' do + let(:obj) { MergeRequest.new } + + it 'raises an error' do + expect { subject }.to raise_error('Unexpected parent: MergeRequest') + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb index 0ff0895b861..3d393e6dcb5 100644 --- a/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/limitable_shared_examples.rb @@ -1,6 +1,30 @@ # frozen_string_literal: true RSpec.shared_examples 'includes Limitable concern' do + describe '#exceeds_limits?' do + let(:plan_limits) { create(:plan_limits, :default_plan) } + + context 'without plan limits configured' do + it { expect(subject.exceeds_limits?).to eq false } + end + + context 'without plan limits configured' do + before do + plan_limits.update!(subject.class.limit_name => 1) + end + + it { expect(subject.exceeds_limits?).to eq false } + + context 'with an existing model' do + before do + subject.clone.save! + end + + it { expect(subject.exceeds_limits?).to eq true } + end + end + end + describe 'validations' do let(:plan_limits) { create(:plan_limits, :default_plan) } diff --git a/spec/tasks/gitlab/db/validate_config_rake_spec.rb b/spec/tasks/gitlab/db/validate_config_rake_spec.rb index 0b2c844a91f..b4bad3dd7b9 100644 --- a/spec/tasks/gitlab/db/validate_config_rake_spec.rb +++ b/spec/tasks/gitlab/db/validate_config_rake_spec.rb @@ -3,6 +3,10 @@ require 'rake_helper' RSpec.describe 'gitlab:db:validate_config', :silence_stdout do + # We don't need to delete this data since it only modifies `ar_internal_metadata` + # which would not be cleaned either by `DbCleaner` + self.use_transactional_tests = false + before :all do Rake.application.rake_require 'active_record/railties/databases' Rake.application.rake_require 'tasks/seed_fu' @@ -111,6 +115,26 @@ RSpec.describe 'gitlab:db:validate_config', :silence_stdout do end it_behaves_like 'validates successfully' + + context 'when config is pointing to incorrect server' do + let(:test_config) do + { + main: main_database_config.merge(port: 11235) + } + end + + it_behaves_like 'validates successfully' + end + + context 'when config is pointing to non-existent database' do + let(:test_config) do + { + main: main_database_config.merge(database: 'non_existent_database') + } + end + + it_behaves_like 'validates successfully' + end end context 'when main: uses database_tasks=false' do diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb index 2456acef53f..3241c931dc5 100644 --- a/spec/workers/build_success_worker_spec.rb +++ b/spec/workers/build_success_worker_spec.rb @@ -28,22 +28,10 @@ RSpec.describe BuildSuccessWorker do it 'does not stop the environment' do expect(environment).to be_available - stub_feature_flags(env_stopped_on_stop_success: true) - subject expect(environment.reload).not_to be_stopped end - - it 'does stop the environment when feature flag is disabled' do - expect(environment).to be_available - - stub_feature_flags(env_stopped_on_stop_success: false) - - subject - - expect(environment.reload).to be_stopped - end end end end |