Commit bc028841 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch 'fj-36079-snippet-graphql-endpoints' into 'master'

Create GraphQL API for Snippets

See merge request gitlab-org/gitlab!20613
parents bf2e083d a2358aaf
# frozen_string_literal: true
module ResolvesSnippets
extend ActiveSupport::Concern
included do
type Types::SnippetType, null: false
argument :ids, [GraphQL::ID_TYPE],
required: false,
description: 'Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"'
argument :visibility, Types::Snippets::VisibilityScopesEnum,
required: false,
description: 'The visibility of the snippet'
end
def resolve(**args)
resolve_snippets(args)
end
private
def resolve_snippets(args)
SnippetsFinder.new(context[:current_user], snippet_finder_params(args)).execute
end
def snippet_finder_params(args)
{
ids: resolve_ids(args[:ids]),
scope: args[:visibility]
}.merge(options_by_type(args[:type]))
end
def resolve_ids(ids)
Array.wrap(ids).map { |id| resolve_gid(id, :id) }
end
def resolve_gid(gid, argument)
return unless gid.present?
GlobalID.parse(gid)&.model_id.tap do |id|
raise Gitlab::Graphql::Errors::ArgumentError, "Invalid global id format for param #{argument}" if id.nil?
end
end
def options_by_type(type)
case type
when 'personal'
{ only_personal: true }
when 'project'
{ only_project: true }
else
{}
end
end
end
# frozen_string_literal: true
module Resolvers
module Projects
class SnippetsResolver < BaseResolver
include ResolvesSnippets
alias_method :project, :object
def resolve(**args)
return Snippet.none if project.nil?
super
end
private
def snippet_finder_params(args)
super.merge(project: project)
end
end
end
end
# frozen_string_literal: true
module Resolvers
class SnippetsResolver < BaseResolver
include ResolvesSnippets
ERROR_MESSAGE = 'Filtering by both an author and a project is not supported'
alias_method :user, :object
argument :author_id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of an author'
argument :project_id, GraphQL::ID_TYPE,
required: false,
description: 'The ID of a project'
argument :type, Types::Snippets::TypeEnum,
required: false,
description: 'The type of snippet'
argument :explore,
GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Explore personal snippets'
def resolve(**args)
if args[:author_id].present? && args[:project_id].present?
raise Gitlab::Graphql::Errors::ArgumentError, ERROR_MESSAGE
end
super
end
private
def snippet_finder_params(args)
super
.merge(author: resolve_gid(args[:author_id], :author),
project: resolve_gid(args[:project_id], :project),
explore: args[:explore])
end
end
end
# frozen_string_literal: true
module Resolvers
module Users
class SnippetsResolver < BaseResolver
include ResolvesSnippets
alias_method :user, :object
argument :type, Types::Snippets::TypeEnum,
required: false,
description: 'The type of snippet'
private
def snippet_finder_params(args)
super.merge(author: user)
end
end
end
end
......@@ -15,6 +15,8 @@ module Types
Types::IssueType
when MergeRequest
Types::MergeRequestType
when Snippet
Types::SnippetType
else
raise "Unknown GraphQL type for #{object}"
end
......
......@@ -10,13 +10,19 @@ module Types
:remove_pages, :read_project, :create_merge_request_in,
:read_wiki, :read_project_member, :create_issue, :upload_file,
:read_cycle_analytics, :download_code, :download_wiki_code,
:fork_project, :create_project_snippet, :read_commit_status,
:fork_project, :read_commit_status,
:request_access, :create_pipeline, :create_pipeline_schedule,
:create_merge_request_from, :create_wiki, :push_code,
:create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages, :read_pages_content, :admin_operations
permission_field :create_snippet
def create_snippet
Ability.allowed?(context[:current_user], :create_project_snippet, object)
end
end
end
end
......
# frozen_string_literal: true
module Types
module PermissionTypes
class Snippet < BasePermissionType
graphql_name 'SnippetPermissions'
abilities :create_note, :award_emoji
permission_field :read_snippet, method: :can_read_snippet?
permission_field :update_snippet, method: :can_update_snippet?
permission_field :admin_snippet, method: :can_admin_snippet?
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class User < BasePermissionType
graphql_name 'UserPermissions'
permission_field :create_snippet
def create_snippet
Ability.allowed?(context[:current_user], :create_personal_snippet)
end
end
end
end
......@@ -151,5 +151,11 @@ module Types
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :snippets,
Types::SnippetType.connection_type,
null: true,
description: 'Snippets of the project',
resolver: Resolvers::Projects::SnippetsResolver
end
end
......@@ -29,6 +29,12 @@ module Types
resolver: Resolvers::MetadataResolver,
description: 'Metadata about GitLab'
field :snippets,
Types::SnippetType.connection_type,
null: true,
resolver: Resolvers::SnippetsResolver,
description: 'Find Snippets visible to the current user'
field :echo, GraphQL::STRING_TYPE, null: false, resolver: Resolvers::EchoResolver # rubocop:disable Graphql/Descriptions
end
end
# frozen_string_literal: true
module Types
class SnippetType < BaseObject
graphql_name 'Snippet'
description 'Represents a snippet entry'
implements(Types::Notes::NoteableType)
present_using SnippetPresenter
authorize :read_snippet
expose_permissions Types::PermissionTypes::Snippet
field :id, GraphQL::ID_TYPE,
description: 'Id of the snippet',
null: false
field :title, GraphQL::STRING_TYPE,
description: 'Title of the snippet',
null: false
field :project, Types::ProjectType,
description: 'The project the snippet is associated with',
null: true,
authorize: :read_project,
resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, snippet.project_id).find }
field :author, Types::UserType,
description: 'The owner of the snippet',
null: false,
resolve: -> (snippet, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, snippet.author_id).find }
field :file_name, GraphQL::STRING_TYPE,
description: 'File Name of the snippet',
null: true
field :content, GraphQL::STRING_TYPE,
description: 'Content of the snippet',
null: false
field :description, GraphQL::STRING_TYPE,
description: 'Description of the snippet',
null: true
field :visibility, GraphQL::STRING_TYPE,
description: 'Visibility of the snippet',
null: false
field :created_at, Types::TimeType,
description: 'Timestamp this snippet was created',
null: false
field :updated_at, Types::TimeType,
description: 'Timestamp this snippet was updated',
null: false
field :web_url, type: GraphQL::STRING_TYPE,
description: 'Web URL of the snippet',
null: false
field :raw_url, type: GraphQL::STRING_TYPE,
description: 'Raw URL of the snippet',
null: false
markdown_field :description_html, null: true, method: :description
end
end
# frozen_string_literal: true
module Types
module Snippets
class TypeEnum < BaseEnum
value 'personal', value: 'personal'
value 'project', value: 'project'
end
end
end
# frozen_string_literal: true
module Types
module Snippets
class VisibilityScopesEnum < BaseEnum
value 'private', value: 'are_private'
value 'internal', value: 'are_internal'
value 'public', value: 'are_public'
end
end
end
......@@ -8,6 +8,8 @@ module Types
present_using UserPresenter
expose_permissions Types::PermissionTypes::User
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Human-readable name of the user'
field :username, GraphQL::STRING_TYPE, null: false,
......@@ -19,5 +21,11 @@ module Types
field :todos, Types::TodoType.connection_type, null: false,
resolver: Resolvers::TodoResolver,
description: 'Todos of the user'
field :snippets,
Types::SnippetType.connection_type,
null: true,
description: 'Snippets authored by the user',
resolver: Resolvers::Users::SnippetsResolver
end
end
......@@ -27,4 +27,7 @@ class PersonalSnippetPolicy < BasePolicy
rule { can?(:create_note) }.enable :award_emoji
rule { can?(:read_all_resources) }.enable :read_personal_snippet
# Aliasing the ability to ease GraphQL permissions check
rule { can?(:read_personal_snippet) }.enable :read_snippet
end
......@@ -45,6 +45,9 @@ class ProjectSnippetPolicy < BasePolicy
end
rule { ~can?(:read_project_snippet) }.prevent :create_note
# Aliasing the ability to ease GraphQL permissions check
rule { can?(:read_project_snippet) }.enable :read_snippet
end
ProjectSnippetPolicy.prepend_if_ee('EE::ProjectSnippetPolicy')
# frozen_string_literal: true
class SnippetPresenter < Gitlab::View::Presenter::Delegated
presents :snippet
def web_url
Gitlab::UrlBuilder.build(snippet)
end
def raw_url
Gitlab::UrlBuilder.build(snippet, raw: true)
end
def can_read_snippet?
can_access_resource?("read")
end
def can_update_snippet?
can_access_resource?("update")
end
def can_admin_snippet?
can_access_resource?("admin")
end
private
def can_access_resource?(ability_prefix)
can?(current_user, ability_name(ability_prefix), snippet)
end
def ability_name(ability_prefix)
"#{ability_prefix}_#{snippet.class.underscore}".to_sym
end
end
---
title: Add Snippet GraphQL resolver endpoints
merge_request: 20613
author:
type: added
......@@ -61,6 +61,7 @@ The GraphQL API includes the following queries at the root level:
1. `namespace` : Within a namespace it is also possible to fetch `projects`.
1. `currentUser`: Information about the currently logged in user.
1. `metaData`: Metadata about GitLab and the GraphQL API.
1. `snippets`: Snippets visible to the currently logged in user.
Root-level queries are defined in
[`app/graphql/types/query_type.rb`](https://gitlab.com/gitlab-org/gitlab/blob/master/app/graphql/types/query_type.rb).
......
......@@ -4512,6 +4512,41 @@ type Project {
"""
sharedRunnersEnabled: Boolean
"""
Snippets of the project
"""
snippets(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
"""
(deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead
"""
......@@ -4675,9 +4710,9 @@ type ProjectPermissions {
createPipelineSchedule: Boolean!
"""
Whether or not a user can perform `create_project_snippet` on this resource
Whether or not a user can perform `create_snippet` on this resource
"""
createProjectSnippet: Boolean!
createSnippet: Boolean!
"""
Whether or not a user can perform `create_wiki` on this resource
......@@ -4882,6 +4917,61 @@ type Query {
"""
fullPath: ID!
): Project
"""
Find Snippets visible to the current user
"""
snippets(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
The ID of an author
"""
authorId: ID
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Explore personal snippets
"""
explore: Boolean
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The ID of a project
"""
projectId: ID
"""
The type of snippet
"""
type: TypeEnum
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
}
"""
......@@ -5137,6 +5227,193 @@ enum SentryErrorStatus {
UNRESOLVED
}
"""
Represents a snippet entry
"""
type Snippet implements Noteable {
"""
The owner of the snippet
"""
author: User!
"""
Content of the snippet
"""
content: String!
"""
Timestamp this snippet was created
"""
createdAt: Time!
"""
Description of the snippet
"""
description: String
"""
The GitLab Flavored Markdown rendering of `description`
"""
descriptionHtml: String
"""
All discussions on this noteable
"""
discussions(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): DiscussionConnection!
"""
File Name of the snippet
"""
fileName: String
"""
Id of the snippet
"""
id: ID!
"""
All notes on this noteable
"""
notes(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): NoteConnection!
"""
The project the snippet is associated with
"""
project: Project
"""
Raw URL of the snippet
"""
rawUrl: String!
"""
Title of the snippet
"""
title: String!
"""
Timestamp this snippet was updated
"""
updatedAt: Time!
"""
Permissions for the current user on the resource
"""
userPermissions: SnippetPermissions!
"""
Visibility of the snippet
"""
visibility: String!
"""
Web URL of the snippet
"""
webUrl: String!
}
"""
The connection type for Snippet.
"""
type SnippetConnection {
"""
A list of edges.
"""
edges: [SnippetEdge]
"""
A list of nodes.
"""
nodes: [Snippet]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type SnippetEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: Snippet
}
type SnippetPermissions {
"""
Whether or not a user can perform `admin_snippet` on this resource
"""
adminSnippet: Boolean!
"""
Whether or not a user can perform `award_emoji` on this resource
"""
awardEmoji: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `read_snippet` on this resource
"""
readSnippet: Boolean!
"""
Whether or not a user can perform `update_snippet` on this resource
"""
updateSnippet: Boolean!
}
type Submodule implements Entry {
flatPath: String!
id: ID!
......@@ -5602,6 +5879,11 @@ type TreeEntryEdge {
node: TreeEntry
}
enum TypeEnum {
personal
project
}
"""
Autogenerated input type of UpdateEpic
"""
......@@ -5740,6 +6022,46 @@ type User {
"""
name: String!
"""
Snippets authored by the user
"""
snippets(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of global snippet ids, e.g., "gid://gitlab/ProjectSnippet/1"
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
The type of snippet
"""
type: TypeEnum
"""
The visibility of the snippet
"""
visibility: VisibilityScopesEnum
): SnippetConnection
"""
Todos of the user
"""
......@@ -5795,6 +6117,11 @@ type User {
type: [TodoTargetEnum!]
): TodoConnection!
"""
Permissions for the current user on the resource
"""
userPermissions: UserPermissions!
"""
Username of the user. Unique within this instance of GitLab
"""
......@@ -5840,3 +6167,16 @@ type UserEdge {
"""
node: User
}
type UserPermissions {
"""
Whether or not a user can perform `create_snippet` on this resource
"""
createSnippet: Boolean!
}
enum VisibilityScopesEnum {
internal
private
public
}
\ No newline at end of file
......@@ -689,7 +689,6 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `downloadCode` | Boolean! | Whether or not a user can perform `download_code` on this resource |
| `downloadWikiCode` | Boolean! | Whether or not a user can perform `download_wiki_code` on this resource |
| `forkProject` | Boolean! | Whether or not a user can perform `fork_project` on this resource |
| `createProjectSnippet` | Boolean! | Whether or not a user can perform `create_project_snippet` on this resource |
| `readCommitStatus` | Boolean! | Whether or not a user can perform `read_commit_status` on this resource |
| `requestAccess` | Boolean! | Whether or not a user can perform `request_access` on this resource |
| `createPipeline` | Boolean! | Whether or not a user can perform `create_pipeline` on this resource |
......@@ -710,6 +709,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `destroyPages` | Boolean! | Whether or not a user can perform `destroy_pages` on this resource |
| `readPagesContent` | Boolean! | Whether or not a user can perform `read_pages_content` on this resource |
| `adminOperations` | Boolean! | Whether or not a user can perform `admin_operations` on this resource |
| `createSnippet` | Boolean! | Whether or not a user can perform `create_snippet` on this resource |
| `readDesign` | Boolean! | Whether or not a user can perform `read_design` on this resource |
| `createDesign` | Boolean! | Whether or not a user can perform `create_design` on this resource |
| `destroyDesign` | Boolean! | Whether or not a user can perform `destroy_design` on this resource |
......@@ -787,6 +787,35 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `time` | Time! | Time the error frequency stats were recorded |
| `count` | Int! | Count of errors received since the previously recorded time |
### Snippet
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | SnippetPermissions! | Permissions for the current user on the resource |
| `id` | ID! | Id of the snippet |
| `title` | String! | Title of the snippet |
| `project` | Project | The project the snippet is associated with |
| `author` | User! | The owner of the snippet |
| `fileName` | String | File Name of the snippet |
| `content` | String! | Content of the snippet |
| `description` | String | Description of the snippet |
| `visibility` | String! | Visibility of the snippet |
| `createdAt` | Time! | Timestamp this snippet was created |
| `updatedAt` | Time! | Timestamp this snippet was updated |
| `webUrl` | String! | Web URL of the snippet |
| `rawUrl` | String! | Raw URL of the snippet |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
### SnippetPermissions
| Name | Type | Description |
| --- | ---- | ---------- |
| `createNote` | Boolean! | Whether or not a user can perform `create_note` on this resource |
| `awardEmoji` | Boolean! | Whether or not a user can perform `award_emoji` on this resource |
| `readSnippet` | Boolean! | Whether or not a user can perform `read_snippet` on this resource |
| `updateSnippet` | Boolean! | Whether or not a user can perform `update_snippet` on this resource |
| `adminSnippet` | Boolean! | Whether or not a user can perform `admin_snippet` on this resource |
### Submodule
| Name | Type | Description |
......@@ -892,7 +921,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| Name | Type | Description |
| --- | ---- | ---------- |
| `userPermissions` | UserPermissions! | Permissions for the current user on the resource |
| `name` | String! | Human-readable name of the user |
| `username` | String! | Username of the user. Unique within this instance of GitLab |
| `avatarUrl` | String! | URL of the user's avatar |
| `webUrl` | String! | Web URL of the user |
### UserPermissions
| Name | Type | Description |
| --- | ---- | ---------- |
| `createSnippet` | Boolean! | Whether or not a user can perform `create_snippet` on this resource |
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::Projects::SnippetsResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, :private, author: current_user) }
let_it_be(:project_snippet) { create(:project_snippet, :internal, author: current_user, project: project) }
let_it_be(:other_project_snippet) { create(:project_snippet, :public, author: other_user, project: project) }
before do
project.add_developer(current_user)
end
it 'calls SnippetsFinder' do
expect_next_instance_of(SnippetsFinder) do |finder|
expect(finder).to receive(:execute)
end
resolve_snippets
end
context 'when using no filter' do
it 'returns expected snippets' do
expect(resolve_snippets).to contain_exactly(project_snippet, other_project_snippet)
end
end
context 'when using filters' do
it 'returns the snippets by visibility' do
aggregate_failures do
expect(resolve_snippets(args: { visibility: 'are_private' })).to be_empty
expect(resolve_snippets(args: { visibility: 'are_internal' })).to contain_exactly(project_snippet)
expect(resolve_snippets(args: { visibility: 'are_public' })).to contain_exactly(other_project_snippet)
end
end
it 'returns the snippets by gid' do
snippets = resolve_snippets(args: { ids: project_snippet.to_global_id })
expect(snippets).to contain_exactly(project_snippet)
end
it 'returns the snippets by array of gid' do
args = {
ids: [project_snippet.to_global_id, other_project_snippet.to_global_id]
}
snippets = resolve_snippets(args: args)
expect(snippets).to contain_exactly(project_snippet, other_project_snippet)
end
it 'returns an error if the gid is invalid' do
expect do
resolve_snippets(args: { ids: 'foo' })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
context 'when no project is provided' do
it 'returns no snippets' do
expect(resolve_snippets(obj: nil)).to be_empty
end
end
context 'when provided user is not current user' do
it 'returns no snippets' do
expect(resolve_snippets(context: { current_user: other_user }, args: { ids: project_snippet.to_global_id })).to be_empty
end
end
end
def resolve_snippets(args: {}, context: { current_user: current_user }, obj: project)
resolve(described_class, obj: obj, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::SnippetsResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:personal_snippet) { create(:personal_snippet, :private, author: current_user) }
let_it_be(:other_personal_snippet) { create(:personal_snippet, :internal, author: other_user) }
let_it_be(:project_snippet) { create(:project_snippet, :internal, author: current_user, project: project) }
let_it_be(:other_project_snippet) { create(:project_snippet, :public, author: other_user, project: project) }
before do
project.add_developer(current_user)
end
it 'calls SnippetsFinder' do
expect_next_instance_of(SnippetsFinder) do |finder|
expect(finder).to receive(:execute)
end
resolve_snippets
end
context 'when using no filter' do
it 'returns expected snippets' do
expect(resolve_snippets).to contain_exactly(personal_snippet, other_personal_snippet, project_snippet, other_project_snippet)
end
end
context 'when using filters' do
context 'by author id' do
it 'returns the snippets' do
snippets = resolve_snippets(args: { author_id: current_user.to_global_id })
expect(snippets).to contain_exactly(personal_snippet, project_snippet)
end
it 'returns an error if the param id is invalid' do
expect do
resolve_snippets(args: { author_id: 'foo' })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
it 'returns the snippets by type' do
aggregate_failures do
expect(resolve_snippets(args: { type: 'personal' })).to contain_exactly(personal_snippet, other_personal_snippet)
expect(resolve_snippets(args: { type: 'project' })).to contain_exactly(project_snippet, other_project_snippet)
end
end
context 'by project id' do
it 'returns the snippets' do
snippets = resolve_snippets(args: { project_id: project.to_global_id })
expect(snippets).to contain_exactly(project_snippet, other_project_snippet)
end
it 'returns an error if the param id is invalid' do
expect do
resolve_snippets(args: { project_id: 'foo' })
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
it 'returns the snippets by visibility' do
aggregate_failures do
expect(resolve_snippets(args: { visibility: 'are_private' })).to contain_exactly(personal_snippet)
expect(resolve_snippets(args: { visibility: 'are_internal' })).to contain_exactly(project_snippet, other_personal_snippet)
expect(resolve_snippets(args: { visibility: 'are_public' })).to contain_exactly(other_project_snippet)
end
end
it 'returns snippets to explore' do
snippets = resolve_snippets(args: { explore: true })
expect(snippets).to contain_exactly(other_personal_snippet)
end
it 'returns the snippets by single gid' do
snippets = resolve_snippets(args: { ids: personal_snippet.to_global_id })
expect(snippets).to contain_exactly(personal_snippet)
end
it 'returns the snippets by array of gid' do
args = {
ids: [personal_snippet.to_global_id, project_snippet.to_global_id]
}
snippets = resolve_snippets(args: args)
expect(snippets).to contain_exactly(personal_snippet, project_snippet)
end
it 'returns an error if the gid is invalid' do
args = {
ids: [personal_snippet.to_global_id, 'foo']
}
expect do
resolve_snippets(args: args)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
it 'returns an error if both project and author are provided' do
expect do
args = {
author_id: current_user.to_global_id,
project_id: project.to_global_id
}
resolve_snippets(args: args)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
end
def resolve_snippets(args: {})
resolve(described_class, obj: nil, args: args, ctx: { current_user: current_user })
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::Users::SnippetsResolver do
include GraphqlHelpers
describe '#resolve' do
let_it_be(:current_user) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:private_personal_snippet) { create(:personal_snippet, :private, author: current_user) }
let_it_be(:public_personal_snippet) { create(:personal_snippet, :public, author: current_user) }
let_it_be(:other_personal_snippet) { create(:personal_snippet, :internal, author: other_user) }
let_it_be(:internal_project_snippet) { create(:project_snippet, :internal, author: current_user, project: project) }
let_it_be(:other_project_snippet) { create(:project_snippet, :public, author: other_user, project: project) }
before do
project.add_developer(current_user)
end
it 'calls SnippetsFinder' do
expect_next_instance_of(SnippetsFinder) do |finder|
expect(finder).to receive(:execute)
end
resolve_snippets
end
context 'when using no filter' do
it 'returns expected authored snippets' do
expect(resolve_snippets).to contain_exactly(private_personal_snippet, public_personal_snippet, internal_project_snippet)
end
end
context 'when using filters' do
it 'returns the snippets by visibility' do
aggregate_failures do
expect(resolve_snippets(args: { visibility: 'are_private' })).to contain_exactly(private_personal_snippet)
expect(resolve_snippets(args: { visibility: 'are_internal' })).to contain_exactly(internal_project_snippet)
expect(resolve_snippets(args: { visibility: 'are_public' })).to contain_exactly(public_personal_snippet)
end
end
it 'returns the snippets by type' do
aggregate_failures do
expect(resolve_snippets(args: { type: 'personal' })).to contain_exactly(private_personal_snippet, public_personal_snippet)
expect(resolve_snippets(args: { type: 'project' })).to contain_exactly(internal_project_snippet)
end
end
it 'returns the snippets by single gid' do
snippets = resolve_snippets(args: { ids: private_personal_snippet.to_global_id })
expect(snippets).to contain_exactly(private_personal_snippet)
end
it 'returns the snippets by array of gid' do
args = {
ids: [private_personal_snippet.to_global_id, public_personal_snippet.to_global_id]
}
snippets = resolve_snippets(args: args)
expect(snippets).to contain_exactly(private_personal_snippet, public_personal_snippet)
end
it 'returns an error if the gid is invalid' do
args = {
ids: [private_personal_snippet.to_global_id, 'foo']
}
expect do
resolve_snippets(args: args)
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end
end
end
def resolve_snippets(args: {})
resolve(described_class, args: args, ctx: { current_user: current_user }, obj: current_user)
end
end
......@@ -8,7 +8,7 @@ describe Types::PermissionTypes::Project do
:change_namespace, :change_visibility_level, :rename_project, :remove_project, :archive_project,
:remove_fork_project, :remove_pages, :read_project, :create_merge_request_in,
:read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics,
:download_code, :download_wiki_code, :fork_project, :create_project_snippet,
:download_code, :download_wiki_code, :fork_project, :create_snippet,
:read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule,
:create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch,
:admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label,
......
# frozen_string_literal: true
require 'spec_helper'
describe Types::PermissionTypes::Snippet do
it 'returns the snippets permissions' do
expected_permissions = [
:create_note, :award_emoji, :read_snippet, :update_snippet, :admin_snippet
]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Types::PermissionTypes::User do
it 'returns user permissions' do
expected_permissions = [
:create_snippet
]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
......@@ -22,7 +22,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_pipeline_succeeds request_access_enabled
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
]
is_expected.to have_graphql_fields(*expected_fields)
......@@ -63,4 +63,13 @@ describe GitlabSchema.types['Project'] do
is_expected.to have_graphql_resolver(Resolvers::MergeRequestsResolver)
end
end
describe 'snippets field' do
subject { described_class.fields['snippets'] }
it 'returns snippets' do
is_expected.to have_graphql_type(Types::SnippetType.connection_type)
is_expected.to have_graphql_resolver(Resolvers::Projects::SnippetsResolver)
end
end
end
......@@ -7,7 +7,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user) }
it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user, :snippets) }
describe 'namespace field' do
subject { described_class.fields['namespace'] }
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Snippet'] do
it 'has the correct fields' do
expected_fields = [:id, :title, :project, :author,
:file_name, :content, :description,
:visibility, :created_at, :updated_at,
:web_url, :raw_url, :notes, :discussions,
:user_permissions, :description_html]
is_expected.to have_graphql_fields(*expected_fields)
end
describe 'authorizations' do
it { expect(described_class).to require_graphql_authorizations(:read_snippet) }
end
end
......@@ -6,4 +6,21 @@ describe GitlabSchema.types['User'] do
it { expect(described_class.graphql_name).to eq('User') }
it { expect(described_class).to require_graphql_authorizations(:read_user) }
it 'has the expected fields' do
expected_fields = %w[
user_permissions snippets name username avatarUrl webUrl todos
]
is_expected.to have_graphql_fields(*expected_fields)
end
describe 'snippets field' do
subject { described_class.fields['snippets'] }
it 'returns snippets' do
is_expected.to have_graphql_type(Types::SnippetType.connection_type)
is_expected.to have_graphql_resolver(Resolvers::Users::SnippetsResolver)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe SnippetPresenter do
include Gitlab::Routing.url_helpers
let_it_be(:user) { create(:user) }
let_it_be(:personal_snippet) { create(:personal_snippet, author: user) }
let_it_be(:project_snippet) { create(:project_snippet, author: user) }
let(:project) { project_snippet.project }
let(:presenter) { described_class.new(snippet, current_user: user) }
before do
project.add_developer(user)
end
describe '#web_url' do
subject { presenter.web_url }
context 'with PersonalSnippet' do
let(:snippet) { personal_snippet }
it 'returns snippet web url' do
expect(subject).to match "/snippets/#{snippet.id}"
end
end
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
it 'returns snippet web url' do
expect(subject).to match "/#{project.full_path}/snippets/#{snippet.id}"
end
end
end
describe '#raw_url' do
subject { presenter.raw_url }
context 'with PersonalSnippet' do
let(:snippet) { personal_snippet }
it 'returns snippet web url' do
expect(subject).to match "/snippets/#{snippet.id}/raw"
end
end
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
it 'returns snippet web url' do
expect(subject).to match "/#{project.full_path}/snippets/#{snippet.id}/raw"
end
end
end
describe '#can_read_snippet?' do
subject { presenter.can_read_snippet? }
context 'with PersonalSnippet' do
let(:snippet) { personal_snippet }
it 'checks read_personal_snippet' do
expect(presenter).to receive(:can?).with(user, :read_personal_snippet, snippet)
subject
end
end
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
it 'checks read_project_snippet ' do
expect(presenter).to receive(:can?).with(user, :read_project_snippet, snippet)
subject
end
end
end
describe '#can_update_snippet?' do
subject { presenter.can_update_snippet? }
context 'with PersonalSnippet' do
let(:snippet) { personal_snippet }
it 'checks update_personal_snippet' do
expect(presenter).to receive(:can?).with(user, :update_personal_snippet, snippet)
subject
end
end
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
it 'checks update_project_snippet ' do
expect(presenter).to receive(:can?).with(user, :update_project_snippet, snippet)
subject
end
end
end
describe '#can_admin_snippet?' do
subject { presenter.can_admin_snippet? }
context 'with PersonalSnippet' do
let(:snippet) { personal_snippet }
it 'checks admin_personal_snippet' do
expect(presenter).to receive(:can?).with(user, :admin_personal_snippet, snippet)
subject
end
end
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
it 'checks admin_project_snippet ' do
expect(presenter).to receive(:can?).with(user, :admin_project_snippet, snippet)
subject
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment