From a2358aaf4e88495789ae7a3330bbcaa3c3f687a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Javier=20L=C3=B3pez?= Date: Thu, 21 Nov 2019 19:54:42 +0100 Subject: [PATCH] Add Snippet GraphQL resolver API Added resolvers for project and user snippets. --- .../resolvers/concerns/resolves_snippets.rb | 57 + .../resolvers/projects/snippets_resolver.rb | 23 + app/graphql/resolvers/snippets_resolver.rb | 45 + .../resolvers/users/snippets_resolver.rb | 21 + app/graphql/types/notes/noteable_type.rb | 2 + app/graphql/types/permission_types/project.rb | 8 +- app/graphql/types/permission_types/snippet.rb | 15 + app/graphql/types/permission_types/user.rb | 15 + app/graphql/types/project_type.rb | 6 + app/graphql/types/query_type.rb | 6 + app/graphql/types/snippet_type.rb | 69 + app/graphql/types/snippets/type_enum.rb | 10 + .../types/snippets/visibility_scopes_enum.rb | 11 + app/graphql/types/user_type.rb | 8 + app/policies/personal_snippet_policy.rb | 3 + app/policies/project_snippet_policy.rb | 3 + app/presenters/snippet_presenter.rb | 35 + .../fj-36079-snippet-graphql-endpoints.yml | 5 + doc/api/graphql/index.md | 1 + .../graphql/reference/gitlab_schema.graphql | 344 +++- doc/api/graphql/reference/gitlab_schema.json | 1469 ++++++++++++++--- doc/api/graphql/reference/index.md | 38 +- .../projects/snippets_resolver_spec.rb | 83 + .../resolvers/snippets_resolver_spec.rb | 128 ++ .../resolvers/users/snippets_resolver_spec.rb | 84 + .../types/permission_types/project_spec.rb | 2 +- .../types/permission_types/snippet_spec.rb | 15 + .../types/permission_types/user_spec.rb | 15 + spec/graphql/types/project_type_spec.rb | 11 +- spec/graphql/types/query_type_spec.rb | 2 +- spec/graphql/types/snippet_type_spec.rb | 19 + spec/graphql/types/user_type_spec.rb | 17 + spec/presenters/snippet_presenter_spec.rb | 130 ++ 33 files changed, 2449 insertions(+), 251 deletions(-) create mode 100644 app/graphql/resolvers/concerns/resolves_snippets.rb create mode 100644 app/graphql/resolvers/projects/snippets_resolver.rb create mode 100644 app/graphql/resolvers/snippets_resolver.rb create mode 100644 app/graphql/resolvers/users/snippets_resolver.rb create mode 100644 app/graphql/types/permission_types/snippet.rb create mode 100644 app/graphql/types/permission_types/user.rb create mode 100644 app/graphql/types/snippet_type.rb create mode 100644 app/graphql/types/snippets/type_enum.rb create mode 100644 app/graphql/types/snippets/visibility_scopes_enum.rb create mode 100644 app/presenters/snippet_presenter.rb create mode 100644 changelogs/unreleased/fj-36079-snippet-graphql-endpoints.yml create mode 100644 spec/graphql/resolvers/projects/snippets_resolver_spec.rb create mode 100644 spec/graphql/resolvers/snippets_resolver_spec.rb create mode 100644 spec/graphql/resolvers/users/snippets_resolver_spec.rb create mode 100644 spec/graphql/types/permission_types/snippet_spec.rb create mode 100644 spec/graphql/types/permission_types/user_spec.rb create mode 100644 spec/graphql/types/snippet_type_spec.rb create mode 100644 spec/presenters/snippet_presenter_spec.rb diff --git a/app/graphql/resolvers/concerns/resolves_snippets.rb b/app/graphql/resolvers/concerns/resolves_snippets.rb new file mode 100644 index 00000000000000..483372bbf63428 --- /dev/null +++ b/app/graphql/resolvers/concerns/resolves_snippets.rb @@ -0,0 +1,57 @@ +# 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 diff --git a/app/graphql/resolvers/projects/snippets_resolver.rb b/app/graphql/resolvers/projects/snippets_resolver.rb new file mode 100644 index 00000000000000..bf9aa45349ffc8 --- /dev/null +++ b/app/graphql/resolvers/projects/snippets_resolver.rb @@ -0,0 +1,23 @@ +# 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 diff --git a/app/graphql/resolvers/snippets_resolver.rb b/app/graphql/resolvers/snippets_resolver.rb new file mode 100644 index 00000000000000..530a288a25bcd4 --- /dev/null +++ b/app/graphql/resolvers/snippets_resolver.rb @@ -0,0 +1,45 @@ +# 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 diff --git a/app/graphql/resolvers/users/snippets_resolver.rb b/app/graphql/resolvers/users/snippets_resolver.rb new file mode 100644 index 00000000000000..d757640b5ff231 --- /dev/null +++ b/app/graphql/resolvers/users/snippets_resolver.rb @@ -0,0 +1,21 @@ +# 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 diff --git a/app/graphql/types/notes/noteable_type.rb b/app/graphql/types/notes/noteable_type.rb index ab4a170b123074..2ac6645284179f 100644 --- a/app/graphql/types/notes/noteable_type.rb +++ b/app/graphql/types/notes/noteable_type.rb @@ -15,6 +15,8 @@ def resolve_type(object, context) Types::IssueType when MergeRequest Types::MergeRequestType + when Snippet + Types::SnippetType else raise "Unknown GraphQL type for #{object}" end diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 3a6ba371154476..2879dbd2b5c65f 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -10,13 +10,19 @@ class Project < BasePermissionType :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 diff --git a/app/graphql/types/permission_types/snippet.rb b/app/graphql/types/permission_types/snippet.rb new file mode 100644 index 00000000000000..1e21efe790a63d --- /dev/null +++ b/app/graphql/types/permission_types/snippet.rb @@ -0,0 +1,15 @@ +# 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 diff --git a/app/graphql/types/permission_types/user.rb b/app/graphql/types/permission_types/user.rb new file mode 100644 index 00000000000000..dba4de2daccc79 --- /dev/null +++ b/app/graphql/types/permission_types/user.rb @@ -0,0 +1,15 @@ +# 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 diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index d2a163b70db42b..a11676770a9045 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -151,5 +151,11 @@ class ProjectType < BaseObject 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 diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 996bf225976192..06188d99490a81 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -29,6 +29,12 @@ class QueryType < ::Types::BaseObject 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 diff --git a/app/graphql/types/snippet_type.rb b/app/graphql/types/snippet_type.rb new file mode 100644 index 00000000000000..3b4dce1d486287 --- /dev/null +++ b/app/graphql/types/snippet_type.rb @@ -0,0 +1,69 @@ +# 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 diff --git a/app/graphql/types/snippets/type_enum.rb b/app/graphql/types/snippets/type_enum.rb new file mode 100644 index 00000000000000..243f05359dbc16 --- /dev/null +++ b/app/graphql/types/snippets/type_enum.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Types + module Snippets + class TypeEnum < BaseEnum + value 'personal', value: 'personal' + value 'project', value: 'project' + end + end +end diff --git a/app/graphql/types/snippets/visibility_scopes_enum.rb b/app/graphql/types/snippets/visibility_scopes_enum.rb new file mode 100644 index 00000000000000..5488e05b95daac --- /dev/null +++ b/app/graphql/types/snippets/visibility_scopes_enum.rb @@ -0,0 +1,11 @@ +# 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 diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb index b45c7893e75cb9..3943c891335454 100644 --- a/app/graphql/types/user_type.rb +++ b/app/graphql/types/user_type.rb @@ -8,6 +8,8 @@ class UserType < BaseObject 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 @@ class UserType < BaseObject 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 diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 5c62bdd0d95817..c2fcf1a10109e2 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -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 diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index d9d09eb04cdeec..076492c6823971 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -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') diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb new file mode 100644 index 00000000000000..ca8ae8d60c49f4 --- /dev/null +++ b/app/presenters/snippet_presenter.rb @@ -0,0 +1,35 @@ +# 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 diff --git a/changelogs/unreleased/fj-36079-snippet-graphql-endpoints.yml b/changelogs/unreleased/fj-36079-snippet-graphql-endpoints.yml new file mode 100644 index 00000000000000..aeb4592d36eee6 --- /dev/null +++ b/changelogs/unreleased/fj-36079-snippet-graphql-endpoints.yml @@ -0,0 +1,5 @@ +--- +title: Add Snippet GraphQL resolver endpoints +merge_request: 20613 +author: +type: added diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index b5b17102836b62..f86802d44635d2 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -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). diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 12cb4aa78eea4e..90ec122aaa23e8 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -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 """ @@ -5839,4 +6166,17 @@ type UserEdge { The item at the end of the edge. """ 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 diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index a9ea09482177ec..f601997162167f 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -173,6 +173,127 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "snippets", + "description": "Find Snippets visible to the current user", + "args": [ + { + "name": "ids", + "description": "Array of global snippet ids, e.g., \"gid://gitlab/ProjectSnippet/1\"", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "visibility", + "description": "The visibility of the snippet", + "type": { + "kind": "ENUM", + "name": "VisibilityScopesEnum", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "authorId", + "description": "The ID of an author", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "projectId", + "description": "The ID of a project", + "type": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "type", + "description": "The type of snippet", + "type": { + "kind": "ENUM", + "name": "TypeEnum", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "explore", + "description": "Explore personal snippets", + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SnippetConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -1207,6 +1328,87 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "snippets", + "description": "Snippets of the project", + "args": [ + { + "name": "ids", + "description": "Array of global snippet ids, e.g., \"gid://gitlab/ProjectSnippet/1\"", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "visibility", + "description": "The visibility of the snippet", + "type": { + "kind": "ENUM", + "name": "VisibilityScopesEnum", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SnippetConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "snippetsEnabled", "description": "(deprecated) Does this project have snippets enabled?. Use `snippets_access_level` instead", @@ -1643,8 +1845,8 @@ "deprecationReason": null }, { - "name": "createProjectSnippet", - "description": "Whether or not a user can perform `create_project_snippet` on this resource", + "name": "createSnippet", + "description": "Whether or not a user can perform `create_snippet` on this resource", "args": [ ], @@ -4431,6 +4633,11 @@ "kind": "OBJECT", "name": "MergeRequest", "ofType": null + }, + { + "kind": "OBJECT", + "name": "Snippet", + "ofType": null } ] }, @@ -4932,12 +5139,12 @@ "deprecationReason": null }, { - "name": "todos", - "description": "Todos of the user", + "name": "snippets", + "description": "Snippets authored by the user", "args": [ { - "name": "action", - "description": "The action to be filtered", + "name": "ids", + "description": "Array of global snippet ids, e.g., \"gid://gitlab/ProjectSnippet/1\"", "type": { "kind": "LIST", "name": null, @@ -4945,8 +5152,8 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "ENUM", - "name": "TodoActionEnum", + "kind": "SCALAR", + "name": "ID", "ofType": null } } @@ -4954,69 +5161,160 @@ "defaultValue": null }, { - "name": "authorId", - "description": "The ID of an author", + "name": "visibility", + "description": "The visibility of the snippet", "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } + "kind": "ENUM", + "name": "VisibilityScopesEnum", + "ofType": null }, "defaultValue": null }, { - "name": "projectId", - "description": "The ID of a project", + "name": "type", + "description": "The type of snippet", "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } + "kind": "ENUM", + "name": "TypeEnum", + "ofType": null }, "defaultValue": null }, { - "name": "groupId", - "description": "The ID of a group", + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - } + "kind": "SCALAR", + "name": "String", + "ofType": null }, "defaultValue": null }, { - "name": "state", - "description": "The state of the todo", + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "NON_NULL", - "name": null, - "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "SnippetConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "todos", + "description": "Todos of the user", + "args": [ + { + "name": "action", + "description": "The action to be filtered", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "TodoActionEnum", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "authorId", + "description": "The ID of an author", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "projectId", + "description": "The ID of a project", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "groupId", + "description": "The ID of a group", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + } + }, + "defaultValue": null + }, + { + "name": "state", + "description": "The state of the todo", + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { "kind": "ENUM", "name": "TodoStateEnum", "ofType": null @@ -5096,6 +5394,24 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "userPermissions", + "description": "Permissions for the current user on the resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "UserPermissions", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "username", "description": "Username of the user. Unique within this instance of GitLab", @@ -5140,6 +5456,37 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "UserPermissions", + "description": null, + "fields": [ + { + "name": "createSnippet", + "description": "Whether or not a user can perform `create_snippet` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "TodoConnection", @@ -5538,21 +5885,21 @@ }, { "kind": "OBJECT", - "name": "Discussion", - "description": null, + "name": "SnippetConnection", + "description": "The connection type for Snippet.", "fields": [ { - "name": "createdAt", - "description": null, + "name": "edges", + "description": "A list of edges.", "args": [ ], "type": { - "kind": "NON_NULL", + "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "Time", + "kind": "OBJECT", + "name": "SnippetEdge", "ofType": null } }, @@ -5560,74 +5907,17 @@ "deprecationReason": null }, { - "name": "id", - "description": null, + "name": "nodes", + "description": "A list of nodes.", "args": [ ], "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "ID", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "notes", - "description": "All notes in the discussion", - "args": [ - { - "name": "after", - "description": "Returns the elements in the list that come after the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "before", - "description": "Returns the elements in the list that come before the specified cursor.", - "type": { - "kind": "SCALAR", - "name": "String", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "first", - "description": "Returns the first _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - }, - { - "name": "last", - "description": "Returns the last _n_ elements from the list.", - "type": { - "kind": "SCALAR", - "name": "Int", - "ofType": null - }, - "defaultValue": null - } - ], - "type": { - "kind": "NON_NULL", + "kind": "LIST", "name": null, "ofType": { "kind": "OBJECT", - "name": "NoteConnection", + "name": "Snippet", "ofType": null } }, @@ -5635,8 +5925,8 @@ "deprecationReason": null }, { - "name": "replyId", - "description": "The ID used to reply to this discussion", + "name": "pageInfo", + "description": "Information to aid in pagination.", "args": [ ], @@ -5644,8 +5934,8 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "SCALAR", - "name": "ID", + "kind": "OBJECT", + "name": "PageInfo", "ofType": null } }, @@ -5662,12 +5952,12 @@ }, { "kind": "OBJECT", - "name": "DiffPosition", - "description": null, + "name": "SnippetEdge", + "description": "An edge in a connection.", "fields": [ { - "name": "diffRefs", - "description": null, + "name": "cursor", + "description": "A cursor for use in pagination.", "args": [ ], @@ -5675,7 +5965,810 @@ "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Snippet", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Snippet", + "description": "Represents a snippet entry", + "fields": [ + { + "name": "author", + "description": "The owner of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "User", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "content", + "description": "Content of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createdAt", + "description": "Timestamp this snippet was created", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": "Description of the snippet", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "descriptionHtml", + "description": "The GitLab Flavored Markdown rendering of `description`", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "discussions", + "description": "All discussions on this noteable", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileName", + "description": "File Name of the snippet", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "Id of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notes", + "description": "All notes on this noteable", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NoteConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project", + "description": "The project the snippet is associated with", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Project", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "rawUrl", + "description": "Raw URL of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "title", + "description": "Title of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updatedAt", + "description": "Timestamp this snippet was updated", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "userPermissions", + "description": "Permissions for the current user on the resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "SnippetPermissions", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "visibility", + "description": "Visibility of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "webUrl", + "description": "Web URL of the snippet", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "Noteable", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DiscussionConnection", + "description": "The connection type for Discussion.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DiscussionEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "pageInfo", + "description": "Information to aid in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "PageInfo", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DiscussionEdge", + "description": "An edge in a connection.", + "fields": [ + { + "name": "cursor", + "description": "A cursor for use in pagination.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "node", + "description": "The item at the end of the edge.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Discussion", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Discussion", + "description": null, + "fields": [ + { + "name": "createdAt", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Time", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "notes", + "description": "All notes in the discussion", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "NoteConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "replyId", + "description": "The ID used to reply to this discussion", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "SnippetPermissions", + "description": null, + "fields": [ + { + "name": "adminSnippet", + "description": "Whether or not a user can perform `admin_snippet` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "awardEmoji", + "description": "Whether or not a user can perform `award_emoji` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "createNote", + "description": "Whether or not a user can perform `create_note` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "readSnippet", + "description": "Whether or not a user can perform `read_snippet` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "updateSnippet", + "description": "Whether or not a user can perform `update_snippet` on this resource", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "VisibilityScopesEnum", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "private", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "internal", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "public", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "TypeEnum", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "personal", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DiffPosition", + "description": null, + "fields": [ + { + "name": "diffRefs", + "description": null, + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", "name": "DiffRefs", "ofType": null } @@ -5929,118 +7022,6 @@ ], "possibleTypes": null }, - { - "kind": "OBJECT", - "name": "DiscussionConnection", - "description": "The connection type for Discussion.", - "fields": [ - { - "name": "edges", - "description": "A list of edges.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "DiscussionEdge", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "nodes", - "description": "A list of nodes.", - "args": [ - - ], - "type": { - "kind": "LIST", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "pageInfo", - "description": "Information to aid in pagination.", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "OBJECT", - "name": "PageInfo", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DiscussionEdge", - "description": "An edge in a connection.", - "fields": [ - { - "name": "cursor", - "description": "A cursor for use in pagination.", - "args": [ - - ], - "type": { - "kind": "NON_NULL", - "name": null, - "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null - } - }, - "isDeprecated": false, - "deprecationReason": null - }, - { - "name": "node", - "description": "The item at the end of the edge.", - "args": [ - - ], - "type": { - "kind": "OBJECT", - "name": "Discussion", - "ofType": null - }, - "isDeprecated": false, - "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [ - - ], - "enumValues": null, - "possibleTypes": null - }, { "kind": "OBJECT", "name": "EpicPermissions", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8d105e6c1aba68..9c6a873b0e703b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -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 | diff --git a/spec/graphql/resolvers/projects/snippets_resolver_spec.rb b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb new file mode 100644 index 00000000000000..eef891bf984890 --- /dev/null +++ b/spec/graphql/resolvers/projects/snippets_resolver_spec.rb @@ -0,0 +1,83 @@ +# 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 diff --git a/spec/graphql/resolvers/snippets_resolver_spec.rb b/spec/graphql/resolvers/snippets_resolver_spec.rb new file mode 100644 index 00000000000000..89c350020f01e0 --- /dev/null +++ b/spec/graphql/resolvers/snippets_resolver_spec.rb @@ -0,0 +1,128 @@ +# 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 diff --git a/spec/graphql/resolvers/users/snippets_resolver_spec.rb b/spec/graphql/resolvers/users/snippets_resolver_spec.rb new file mode 100644 index 00000000000000..6412d77e02b0b5 --- /dev/null +++ b/spec/graphql/resolvers/users/snippets_resolver_spec.rb @@ -0,0 +1,84 @@ +# 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 diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb index 6d5a905c128feb..a3a9872ee1a117 100644 --- a/spec/graphql/types/permission_types/project_spec.rb +++ b/spec/graphql/types/permission_types/project_spec.rb @@ -8,7 +8,7 @@ :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, diff --git a/spec/graphql/types/permission_types/snippet_spec.rb b/spec/graphql/types/permission_types/snippet_spec.rb new file mode 100644 index 00000000000000..71843153d43425 --- /dev/null +++ b/spec/graphql/types/permission_types/snippet_spec.rb @@ -0,0 +1,15 @@ +# 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 diff --git a/spec/graphql/types/permission_types/user_spec.rb b/spec/graphql/types/permission_types/user_spec.rb new file mode 100644 index 00000000000000..1e8201db56846f --- /dev/null +++ b/spec/graphql/types/permission_types/user_spec.rb @@ -0,0 +1,15 @@ +# 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 diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 8a697b1bcaed87..5d1a5fe1987cbd 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -22,7 +22,7 @@ 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 @@ 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 diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 1365bc0dc14932..b2d0ba27d4ed5f 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -7,7 +7,7 @@ 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'] } diff --git a/spec/graphql/types/snippet_type_spec.rb b/spec/graphql/types/snippet_type_spec.rb new file mode 100644 index 00000000000000..3c3250a5fa2da9 --- /dev/null +++ b/spec/graphql/types/snippet_type_spec.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb index 8134cc13eb4961..b9174b9a90be0e 100644 --- a/spec/graphql/types/user_type_spec.rb +++ b/spec/graphql/types/user_type_spec.rb @@ -6,4 +6,21 @@ 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 diff --git a/spec/presenters/snippet_presenter_spec.rb b/spec/presenters/snippet_presenter_spec.rb new file mode 100644 index 00000000000000..d874dbcc2792de --- /dev/null +++ b/spec/presenters/snippet_presenter_spec.rb @@ -0,0 +1,130 @@ +# 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 -- GitLab