Commit 3369ed7e authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0f1260a9 a6866652
# frozen_string_literal: true
module Types
class IssuableSearchableFieldEnum < BaseEnum
graphql_name 'IssuableSearchableField'
description 'Fields to perform the search in'
Issuable::SEARCHABLE_FIELDS.each do |field|
value field.upcase, value: field, description: "Search in #{field} field."
end
end
end
......@@ -31,6 +31,7 @@ module Issuable
TITLE_HTML_LENGTH_MAX = 800
DESCRIPTION_LENGTH_MAX = 1.megabyte
DESCRIPTION_HTML_LENGTH_MAX = 5.megabytes
SEARCHABLE_FIELDS = %w(title description).freeze
STATE_ID_MAP = {
opened: 1,
......@@ -264,15 +265,16 @@ module Issuable
# matched_columns - Modify the scope of the query. 'title', 'description' or joining them with a comma.
#
# Returns an ActiveRecord::Relation.
def full_search(query, matched_columns: 'title,description', use_minimum_char_limit: true)
allowed_columns = [:title, :description]
matched_columns = matched_columns.to_s.split(',').map(&:to_sym)
matched_columns &= allowed_columns
def full_search(query, matched_columns: nil, use_minimum_char_limit: true)
if matched_columns
matched_columns = matched_columns.to_s.split(',')
matched_columns &= SEARCHABLE_FIELDS
matched_columns.map!(&:to_sym)
end
# Matching title or description if the matched_columns did not contain any allowed columns.
matched_columns = [:title, :description] if matched_columns.empty?
search_columns = matched_columns.presence || [:title, :description]
fuzzy_search(query, matched_columns, use_minimum_char_limit: use_minimum_char_limit)
fuzzy_search(query, search_columns, use_minimum_char_limit: use_minimum_char_limit)
end
def simple_sorts
......
......@@ -484,7 +484,7 @@ class Integration < ApplicationRecord
# Disable test for instance-level and group-level integrations.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
!(instance_level? || group_level?)
project_level?
end
def project_level?
......
......@@ -7399,6 +7399,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="boardepicancestorsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="boardepicancestorsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="boardepicancestorsiids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="boardepicancestorsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="boardepicancestorsincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="boardepicancestorsincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="boardepicancestorslabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -7431,6 +7432,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="boardepicchildreniid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="boardepicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="boardepicchildreniids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="boardepicchildrenin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="boardepicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="boardepicchildrenincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="boardepicchildrenlabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -8621,6 +8623,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="epicancestorsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="epicancestorsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="epicancestorsiids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="epicancestorsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="epicancestorsincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="epicancestorsincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="epicancestorslabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -8653,6 +8656,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="epicchildreniid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="epicchildreniidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="epicchildreniids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="epicchildrenin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="epicchildrenincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="epicchildrenincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="epicchildrenlabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -9257,6 +9261,7 @@ Returns [`Epic`](#epic).
| <a id="groupepiciid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="groupepiciidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="groupepiciids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="groupepicin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="groupepicincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="groupepicincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="groupepiclabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -9301,6 +9306,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="groupepicsiid"></a>`iid` | [`ID`](#id) | IID of the epic, e.g., "1". |
| <a id="groupepicsiidstartswith"></a>`iidStartsWith` | [`String`](#string) | Filter epics by IID for autocomplete. |
| <a id="groupepicsiids"></a>`iids` | [`[ID!]`](#id) | List of IIDs of epics, e.g., `[1, 2]`. |
| <a id="groupepicsin"></a>`in` | [`[IssuableSearchableField!]`](#issuablesearchablefield) | Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument. |
| <a id="groupepicsincludeancestorgroups"></a>`includeAncestorGroups` | [`Boolean`](#boolean) | Include epics from ancestor groups. |
| <a id="groupepicsincludedescendantgroups"></a>`includeDescendantGroups` | [`Boolean`](#boolean) | Include epics from descendant groups. |
| <a id="groupepicslabelname"></a>`labelName` | [`[String!]`](#string) | Filter epics by labels. |
......@@ -14410,6 +14416,15 @@ Health status of an issue or epic.
| <a id="healthstatusneedsattention"></a>`needsAttention` | |
| <a id="healthstatusontrack"></a>`onTrack` | |
### `IssuableSearchableField`
Fields to perform the search in.
| Value | Description |
| ----- | ----------- |
| <a id="issuablesearchablefielddescription"></a>`DESCRIPTION` | Search in description field. |
| <a id="issuablesearchablefieldtitle"></a>`TITLE` | Search in title field. |
### `IssuableSeverity`
Incident severity.
......
......@@ -21,6 +21,10 @@ module Resolvers
required: false,
description: 'Search query for epic title or description.'
argument :in, [Types::IssuableSearchableFieldEnum],
required: false,
description: 'Specify the fields to perform the search in. Defaults to `[TITLE, DESCRIPTION]`. Requires the `search` argument.'
argument :sort, Types::EpicSortEnum,
required: false,
description: 'List epics by sort order.'
......@@ -68,6 +72,7 @@ module Resolvers
def ready?(**args)
validate_timeframe_params!(args)
validate_starts_with_iid!(args)
validate_search_in_params!(args)
super(**args)
end
......@@ -108,6 +113,7 @@ module Resolvers
transformed = args.dup
transformed[:group_id] = group
transformed[:iids] ||= [args[:iid]].compact
transformed[:in] = args[:in].join(',') if args[:in].present?
transformed.merge(transform_timeframe_parameters(args)).merge(relative_param)
end
......@@ -157,5 +163,12 @@ module Resolvers
raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid `iidStartsWith` query'
end
end
def validate_search_in_params!(args)
return unless args[:in].present? && args[:search].blank?
raise Gitlab::Graphql::Errors::ArgumentError,
'`search` should be present when including the `in` argument'
end
end
end
......@@ -115,6 +115,37 @@ RSpec.describe Resolvers::EpicsResolver do
expect(epics).to match_array([epic2, epic3])
end
context 'with in param' do
it 'returns an error if param search is missing' do
error_message = "`search` should be present when including the `in` argument"
expect { resolve_epics(in: ['title']) }
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, error_message)
end
it 'filters epics by description only' do
epics_with_text = resolve_epics(search: 'text', in: ['description'])
epics_with_created = resolve_epics(search: 'created', in: ['description'])
expect(epics_with_created).to be_empty
expect(epics_with_text).to match_array([epic2, epic3])
end
it 'filters epics by title only' do
epics_with_text = resolve_epics(search: 'text', in: ['title'])
epics_with_created = resolve_epics(search: 'created', in: ['title'])
expect(epics_with_created).to match_array([epic1, epic2])
expect(epics_with_text).to be_empty
end
it 'filters epics by title and description' do
epic4 = create(:epic, group: group, title: 'fourth text', description: ['description'])
epics = resolve_epics(search: 'text', in: %w(title description))
expect(epics).to match_array([epic2, epic3, epic4])
end
end
end
context 'with author_username' do
......
......@@ -213,6 +213,17 @@ RSpec.describe 'Epics through GroupQuery' do
expect_array_response([epic.to_global_id.to_s])
end
end
context 'with search params' do
it 'returns only matching epics' do
filter_params = { search: 'bar', in: [:DESCRIPTION] }
graphql_query = query(filter_params)
post_graphql(graphql_query, current_user: user)
expect_array_response([epic2.to_global_id.to_s])
end
end
end
context 'when error requests' do
......
......@@ -18,17 +18,15 @@ module API
not_found!('Avatar') if avatar.blank?
filename = File.basename(avatar.file.file)
header(
'Content-Disposition',
ActionDispatch::Http::ContentDisposition.format(
disposition: 'attachment',
filename: filename
filename: avatar.filename
)
)
present_carrierwave_file!(user_group.avatar)
present_carrierwave_file!(avatar)
end
end
end
......
......@@ -62,6 +62,8 @@ module API
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
authorize_download_code!
not_found! unless release
present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description]
end
......@@ -177,7 +179,7 @@ module API
end
def authorize_download_code!
authorize! :download_code, release
authorize! :download_code, user_project
end
def authorize_create_evidence!
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::IssuableSearchableFieldEnum do
specify { expect(described_class.graphql_name).to eq('IssuableSearchableField') }
it 'exposes all the issuable searchable fields' do
expect(described_class.values.keys).to contain_exactly(
*Issuable::SEARCHABLE_FIELDS.map(&:upcase)
)
end
end
......@@ -145,35 +145,29 @@ RSpec.describe Integration do
describe '#can_test?' do
subject { integration.can_test? }
context 'when repository is not empty' do
let(:project) { build(:project, :repository) }
it { is_expected.to be true }
end
context 'when repository is empty' do
let(:project) { build(:project) }
context 'when project-level integration' do
let(:project) { create(:project) }
it { is_expected.to be true }
end
context 'when instance-level service' do
context 'when instance-level integration' do
Integration.available_integration_types.each do |type|
let(:integration) do
described_class.send(:integration_type_to_model, type).new(instance: true)
end
it { is_expected.to be_falsey }
it { is_expected.to be false }
end
end
context 'when group-level service' do
context 'when group-level integration' do
Integration.available_integration_types.each do |type|
let(:integration) do
described_class.send(:integration_type_to_model, type).new(group_id: group.id)
end
it { is_expected.to be_falsey }
it { is_expected.to be false }
end
end
end
......
......@@ -18,7 +18,7 @@ RSpec.describe Integrations::Redmine do
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
end
context 'when service is active' do
context 'when integration is active' do
before do
subject.active = true
end
......@@ -27,12 +27,12 @@ RSpec.describe Integrations::Redmine do
it { is_expected.to validate_presence_of(:issues_url) }
it { is_expected.to validate_presence_of(:new_issue_url) }
it_behaves_like 'issue tracker service URL attribute', :project_url
it_behaves_like 'issue tracker service URL attribute', :issues_url
it_behaves_like 'issue tracker service URL attribute', :new_issue_url
it_behaves_like 'issue tracker integration URL attribute', :project_url
it_behaves_like 'issue tracker integration URL attribute', :issues_url
it_behaves_like 'issue tracker integration URL attribute', :new_issue_url
end
context 'when service is inactive' do
context 'when integration is inactive' do
before do
subject.active = false
end
......
......@@ -9,9 +9,9 @@ RSpec.describe API::GroupAvatar do
describe 'GET /groups/:id/avatar' do
context 'when the group is public' do
it 'retrieves the avatar successfully' do
group = create(:group, :public, :with_avatar)
let(:group) { create(:group, :public, :with_avatar) }
it 'retrieves the avatar successfully' do
get api(avatar_path(group))
expect(response).to have_gitlab_http_status(:ok)
......@@ -19,6 +19,22 @@ RSpec.describe API::GroupAvatar do
.to eq(%(attachment; filename="dk.png"; filename*=UTF-8''dk.png))
end
context 'when the avatar is in the object storage' do
before do
stub_uploads_object_storage(AvatarUploader)
group.avatar.migrate!(ObjectStorage::Store::REMOTE)
end
it 'redirects to the file in the object storage' do
get api(avatar_path(group))
expect(response).to have_gitlab_http_status(:found)
expect(response.headers['Content-Disposition'])
.to eq(%(attachment; filename="dk.png"; filename*=UTF-8''dk.png))
end
end
context 'when the group does not have avatar' do
it 'returns :not_found' do
group = create(:group, :public)
......
......@@ -463,9 +463,23 @@ RSpec.describe API::Releases do
end
context 'when specified tag is not found in the project' do
it 'cannot find the release entry' do
it 'returns 404 for maintater' do
get api("/projects/#{project.id}/releases/non_exist_tag", maintainer)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Not Found')
end
it 'returns project not found for no user' do
get api("/projects/#{project.id}/releases/non_exist_tag", nil)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns forbidden for guest' do
get api("/projects/#{project.id}/releases/non_existing_tag", guest)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
......
......@@ -1251,10 +1251,10 @@
dependencies:
"@sinonjs/commons" "^1.7.0"
"@sourcegraph/code-host-integration@0.0.57":
version "0.0.57"
resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.57.tgz#aed4649a51745deef5e4ee79b9a4fdc092471237"
integrity sha512-LLQp58+fqzM1IjAgti4zPwXrVVu2mNC8fpwNVnF23ead6JZPQe6Ap5fhOTZVE7ILQcFt78brGX/49Qib1Hsq0A==
"@sourcegraph/code-host-integration@0.0.58":
version "0.0.58"
resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.58.tgz#7adc78c0a420e7527c68782e2f0c9c62652df02d"
integrity sha512-a1h/qZ1jFf8VBhqWVccGY9VRjjEbqIH7pfY9o2TmuB94aE7bqFgslINkSY6HjefCjDb8akMy//3AjUyFZyYWhQ==
"@stylelint/postcss-css-in-js@^0.37.2":
version "0.37.2"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment