diff --git a/app/graphql/mutations/custom_emoji/create.rb b/app/graphql/mutations/custom_emoji/create.rb new file mode 100644 index 0000000000000000000000000000000000000000..d912a29d12e7b99297378b884a67b5d82e478301 --- /dev/null +++ b/app/graphql/mutations/custom_emoji/create.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Mutations + module CustomEmoji + class Create < BaseMutation + include Mutations::ResolvesGroup + + graphql_name 'CreateCustomEmoji' + + authorize :create_custom_emoji + + field :custom_emoji, + Types::CustomEmojiType, + null: true, + description: 'The new custom emoji' + + argument :group_path, GraphQL::ID_TYPE, + required: true, + description: 'Namespace full path the emoji is associated with' + + argument :name, GraphQL::STRING_TYPE, + required: true, + description: 'Name of the emoji' + + argument :url, GraphQL::STRING_TYPE, + required: true, + as: :file, + description: 'Location of the emoji file' + + def resolve(group_path:, **args) + group = authorized_find!(group_path: group_path) + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911#note_444682238 + args[:external] = true + + custom_emoji = group.custom_emoji.create(args) + + { + custom_emoji: custom_emoji.valid? ? custom_emoji : nil, + errors: errors_on_object(custom_emoji) + } + end + + private + + def find_object(group_path:) + resolve_group(full_path: group_path) + end + end + end +end diff --git a/app/graphql/types/custom_emoji_type.rb b/app/graphql/types/custom_emoji_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..f7d1a7800bca25cb81da6069439a8c16364ea089 --- /dev/null +++ b/app/graphql/types/custom_emoji_type.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Types + class CustomEmojiType < BaseObject + graphql_name 'CustomEmoji' + description 'A custom emoji uploaded by user' + + authorize :read_custom_emoji + + field :id, ::Types::GlobalIDType[::CustomEmoji], + null: false, + description: 'The ID of the emoji' + + field :name, GraphQL::STRING_TYPE, + null: false, + description: 'The name of the emoji' + + field :url, GraphQL::STRING_TYPE, + null: false, + method: :file, + description: 'The link to file of the emoji' + + field :external, GraphQL::BOOLEAN_TYPE, + null: false, + description: 'Whether the emoji is an external link' + end +end diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index d8555684efb314e60619c24203c82f36396ccf67..2f17a4f936510e789b7eef74d14cb8c15582ccf1 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -17,6 +17,10 @@ class GroupType < NamespaceType group.avatar_url(only_path: false) end + field :custom_emoji, Types::CustomEmojiType.connection_type, null: true, + description: 'Custom emoji within this namespace', + feature_flag: :custom_emoji + field :share_with_group_lock, GraphQL::BOOLEAN_TYPE, null: true, description: 'Indicates if sharing a project with another group within this group is prevented' diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 802e98f7d62fd8a35e468c44516f2ac699aa90d0..75ccac6d590b6b55d9b83c55fedc6c1e802cda9e 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -29,6 +29,7 @@ class MutationType < BaseObject mount_mutation Mutations::Boards::Lists::Destroy mount_mutation Mutations::Branches::Create, calls_gitaly: true mount_mutation Mutations::Commits::Create, calls_gitaly: true + mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji mount_mutation Mutations::Discussions::ToggleResolve mount_mutation Mutations::Issues::Create mount_mutation Mutations::Issues::SetAssignees diff --git a/app/graphql/types/permission_types/custom_emoji.rb b/app/graphql/types/permission_types/custom_emoji.rb new file mode 100644 index 0000000000000000000000000000000000000000..0b2e7da44f54b57652d632c356679137ec642e27 --- /dev/null +++ b/app/graphql/types/permission_types/custom_emoji.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Types + module PermissionTypes + class CustomEmoji < BasePermissionType + graphql_name 'CustomEmojiPermissions' + + abilities :create_custom_emoji, :read_custom_emoji + end + end +end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 643b4060ad66564dcdfc2f9f3dd0d8d8da71024d..ed22d4ba231f9a72c2e95e473c4a633fd638ac88 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -3,14 +3,21 @@ class CustomEmoji < ApplicationRecord belongs_to :namespace, inverse_of: :custom_emoji + belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id' + + # For now only external emoji are supported. See https://gitlab.com/gitlab-org/gitlab/-/issues/230467 + validates :external, inclusion: { in: [true] } + + validates :file, public_url: true, if: :external + validate :valid_emoji_name - validates :namespace, presence: true + validates :group, presence: true validates :name, uniqueness: { scope: [:namespace_id, :name] }, presence: true, length: { maximum: 36 }, - format: { with: /\A\w+\z/ } + format: { with: /\A([a-z0-9]+[-_]?)+[a-z0-9]+\z/ } private diff --git a/app/policies/custom_emoji_policy.rb b/app/policies/custom_emoji_policy.rb new file mode 100644 index 0000000000000000000000000000000000000000..ba73b9a378206a9a77cbb342be3b917528c7a078 --- /dev/null +++ b/app/policies/custom_emoji_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class CustomEmojiPolicy < BasePolicy + delegate { @subject.group } +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 6ff6d1359ab9d2d711af7c129f9a35f47c0425a5..664fb40cbcf60a897bc7110a20bb66482b6729c4 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -101,6 +101,7 @@ class GroupPolicy < BasePolicy enable :read_label enable :read_board enable :read_group_member + enable :read_custom_emoji end rule { ~can?(:read_group) }.policy do @@ -114,6 +115,7 @@ class GroupPolicy < BasePolicy enable :create_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation + enable :create_custom_emoji end rule { reporter }.policy do diff --git a/config/feature_flags/development/custom_emoji.yml b/config/feature_flags/development/custom_emoji.yml new file mode 100644 index 0000000000000000000000000000000000000000..64c53c29fdb85f2a5b4f5bcf5102d9381955fb55 --- /dev/null +++ b/config/feature_flags/development/custom_emoji.yml @@ -0,0 +1,8 @@ +--- +name: custom_emoji +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37911 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/231317 +milestone: '13.6' +type: development +group: group::project management +default_enabled: false diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 5bb1dc0eff66aa833825fa8ce8cfbb277240389d..aab580b8888b6e55cf91552e02562d79b6d96a44 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -3834,6 +3834,51 @@ type CreateClusterAgentPayload { errors: [String!]! } +""" +Autogenerated input type of CreateCustomEmoji +""" +input CreateCustomEmojiInput { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + Namespace full path the emoji is associated with + """ + groupPath: ID! + + """ + Name of the emoji + """ + name: String! + + """ + Location of the emoji file + """ + url: String! +} + +""" +Autogenerated return type of CreateCustomEmoji +""" +type CreateCustomEmojiPayload { + """ + A unique identifier for the client performing the mutation. + """ + clientMutationId: String + + """ + The new custom emoji + """ + customEmoji: CustomEmoji + + """ + Errors encountered during execution of the mutation. + """ + errors: [String!]! +} + """ Autogenerated input type of CreateDiffNote """ @@ -4431,6 +4476,71 @@ interface CurrentUserTodos { ): TodoConnection! } +""" +A custom emoji uploaded by user +""" +type CustomEmoji { + """ + Whether the emoji is an external link + """ + external: Boolean! + + """ + The ID of the emoji + """ + id: CustomEmojiID! + + """ + The name of the emoji + """ + name: String! + + """ + The link to file of the emoji + """ + url: String! +} + +""" +The connection type for CustomEmoji. +""" +type CustomEmojiConnection { + """ + A list of edges. + """ + edges: [CustomEmojiEdge] + + """ + A list of nodes. + """ + nodes: [CustomEmoji] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! +} + +""" +An edge in a connection. +""" +type CustomEmojiEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CustomEmoji +} + +""" +Identifier of CustomEmoji +""" +scalar CustomEmojiID + """ Autogenerated input type of DastOnDemandScanCreate """ @@ -8516,6 +8626,31 @@ type Group { """ containsLockedProjects: Boolean! + """ + Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled + """ + customEmoji( + """ + 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 + ): CustomEmojiConnection + """ Description of the namespace """ @@ -13501,6 +13636,11 @@ type Mutation { createBoard(input: CreateBoardInput!): CreateBoardPayload createBranch(input: CreateBranchInput!): CreateBranchPayload createClusterAgent(input: CreateClusterAgentInput!): CreateClusterAgentPayload + + """ + . Available only when feature flag `custom_emoji` is enabled + """ + createCustomEmoji(input: CreateCustomEmojiInput!): CreateCustomEmojiPayload createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload createEpic(input: CreateEpicInput!): CreateEpicPayload createImageDiffNote(input: CreateImageDiffNoteInput!): CreateImageDiffNotePayload diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 6614396da7203afea2c994617ae5af285a81863b..eaad13eeef2f44005a493bf41bc6de88a08027e2 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -10477,6 +10477,136 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "CreateCustomEmojiInput", + "description": "Autogenerated input type of CreateCustomEmoji", + "fields": null, + "inputFields": [ + { + "name": "groupPath", + "description": "Namespace full path the emoji is associated with", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "name", + "description": "Name of the emoji", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "url", + "description": "Location of the emoji file", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + }, + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CreateCustomEmojiPayload", + "description": "Autogenerated return type of CreateCustomEmoji", + "fields": [ + { + "name": "clientMutationId", + "description": "A unique identifier for the client performing the mutation.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "customEmoji", + "description": "The new custom emoji", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "CustomEmoji", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "errors", + "description": "Errors encountered during execution of the mutation.", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "CreateDiffNoteInput", @@ -12100,6 +12230,213 @@ } ] }, + { + "kind": "OBJECT", + "name": "CustomEmoji", + "description": "A custom emoji uploaded by user", + "fields": [ + { + "name": "external", + "description": "Whether the emoji is an external link", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "id", + "description": "The ID of the emoji", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "CustomEmojiID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": "The name of the emoji", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "url", + "description": "The link to file of the emoji", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "CustomEmojiConnection", + "description": "The connection type for CustomEmoji.", + "fields": [ + { + "name": "edges", + "description": "A list of edges.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomEmojiEdge", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "nodes", + "description": "A list of nodes.", + "args": [ + + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "CustomEmoji", + "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": "CustomEmojiEdge", + "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": "CustomEmoji", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "CustomEmojiID", + "description": "Identifier of CustomEmoji", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "DastOnDemandScanCreateInput", @@ -23575,6 +23912,59 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "customEmoji", + "description": "Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled", + "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": "OBJECT", + "name": "CustomEmojiConnection", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "description", "description": "Description of the namespace", @@ -37652,6 +38042,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "createCustomEmoji", + "description": ". Available only when feature flag `custom_emoji` is enabled", + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "CreateCustomEmojiInput", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "OBJECT", + "name": "CreateCustomEmojiPayload", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "createDiffNote", "description": null, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 34d30e8c7f1bc694ca3ba4832581cb67d18e5a00..383024336580ce245ffd508c9f72c97756567859 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -630,6 +630,16 @@ Autogenerated return type of CreateClusterAgent. | `clusterAgent` | ClusterAgent | Cluster agent created after mutation | | `errors` | String! => Array | Errors encountered during execution of the mutation. | +### CreateCustomEmojiPayload + +Autogenerated return type of CreateCustomEmoji. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `clientMutationId` | String | A unique identifier for the client performing the mutation. | +| `customEmoji` | CustomEmoji | The new custom emoji | +| `errors` | String! => Array | Errors encountered during execution of the mutation. | + ### CreateDiffNotePayload Autogenerated return type of CreateDiffNote. @@ -721,6 +731,17 @@ Autogenerated return type of CreateTestCase. | `errors` | String! => Array | Errors encountered during execution of the mutation. | | `testCase` | Issue | The test case created | +### CustomEmoji + +A custom emoji uploaded by user. + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `external` | Boolean! | Whether the emoji is an external link | +| `id` | CustomEmojiID! | The ID of the emoji | +| `name` | String! | The name of the emoji | +| `url` | String! | The link to file of the emoji | + ### DastOnDemandScanCreatePayload Autogenerated return type of DastOnDemandScanCreate. @@ -1392,6 +1413,7 @@ Autogenerated return type of EpicTreeReorder. | `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group. Available only when feature flag `group_coverage_data_report_graph` is enabled | | `containerRepositories` | ContainerRepositoryConnection | Container repositories of the project | | `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit | +| `customEmoji` | CustomEmojiConnection | Custom emoji within this namespace. Available only when feature flag `custom_emoji` is enabled | | `description` | String | Description of the namespace | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `emailsDisabled` | Boolean | Indicates if a group has email notifications disabled | diff --git a/spec/factories/custom_emoji.rb b/spec/factories/custom_emoji.rb index 2d185794ac9967ae52482d4ee0ba9e3f55993e3a..ba1ae11c18d668c307db188f0985a63b3096f4ca 100644 --- a/spec/factories/custom_emoji.rb +++ b/spec/factories/custom_emoji.rb @@ -4,6 +4,7 @@ factory :custom_emoji, class: 'CustomEmoji' do sequence(:name) { |n| "custom_emoji#{n}" } namespace - file { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png')) } + group + file { 'https://gitlab.com/images/partyparrot.png' } end end diff --git a/spec/graphql/types/custom_emoji_type_spec.rb b/spec/graphql/types/custom_emoji_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7f3c99e4b637ab90879d424ac9edec7770b6ad68 --- /dev/null +++ b/spec/graphql/types/custom_emoji_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CustomEmoji'] do + specify { expect(described_class.graphql_name).to eq('CustomEmoji') } + + specify { expect(described_class).to require_graphql_authorizations(:read_custom_emoji) } + + specify { expect(described_class).to have_graphql_fields(:id, :name, :url, :external) } +end diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 836c41391074ae87dfb867b089ec1196da068ce3..62380299ea09b662462718830b63107bd53a1737 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -13,20 +13,28 @@ describe 'exclusion of duplicated emoji' do let(:emoji_name) { Gitlab::Emoji.emojis_names.sample } + let(:group) { create(:group, :private) } it 'disallows emoji names of built-in emoji' do - new_emoji = build(:custom_emoji, name: emoji_name) + new_emoji = build(:custom_emoji, name: emoji_name, group: group) expect(new_emoji).not_to be_valid expect(new_emoji.errors.messages).to eq(name: ["#{emoji_name} is already being used for another emoji"]) end it 'disallows duplicate custom emoji names within namespace' do - old_emoji = create(:custom_emoji) - new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace) + old_emoji = create(:custom_emoji, group: group) + new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group) expect(new_emoji).not_to be_valid expect(new_emoji.errors.messages).to eq(name: ["has already been taken"]) end + + it 'disallows non http and https file value' do + emoji = build(:custom_emoji, name: 'new-name', group: group, file: 'ftp://some-url.in') + + expect(emoji).not_to be_valid + expect(emoji.errors.messages).to eq(file: ["is blocked: Only allowed schemes are http, https"]) + end end end diff --git a/spec/requests/api/graphql/custom_emoji_query_spec.rb b/spec/requests/api/graphql/custom_emoji_query_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d5a423d0ebae09f3ea4141b7328a964244b9e8f3 --- /dev/null +++ b/spec/requests/api/graphql/custom_emoji_query_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'getting custom emoji within namespace' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group, :private) } + let_it_be(:custom_emoji) { create(:custom_emoji, group: group) } + + before do + stub_feature_flags(custom_emoji: true) + group.add_developer(current_user) + end + + describe "Query CustomEmoji on Group" do + def custom_emoji_query(group) + graphql_query_for('group', 'fullPath' => group.full_path) + end + + it 'returns emojis when authorised' do + post_graphql(custom_emoji_query(group), current_user: current_user) + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_data['group']['customEmoji']['nodes'].count). to eq(1) + expect(graphql_data['group']['customEmoji']['nodes'].first['name']). to eq(custom_emoji.name) + end + + it 'returns nil when unauthorised' do + user = create(:user) + post_graphql(custom_emoji_query(group), current_user: user) + + expect(graphql_data['group']).to be_nil + end + end +end diff --git a/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c91437fa355767b0167883b7a4c30a1484358e25 --- /dev/null +++ b/spec/requests/api/graphql/mutations/custom_emoji/create_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Creation of a new Custom Emoji' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:group) { create(:group) } + + let(:attributes) do + { + name: 'my_new_emoji', + url: 'https://example.com/image.png', + group_path: group.full_path + } + end + + let(:mutation) do + graphql_mutation(:create_custom_emoji, attributes) + end + + context 'when the user has no permission' do + it 'does not create custom emoji' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change(CustomEmoji, :count) + end + end + + context 'when user has permission' do + before do + group.add_developer(current_user) + end + + it 'creates custom emoji' do + expect { post_graphql_mutation(mutation, current_user: current_user) }.to change(CustomEmoji, :count).by(1) + + gql_response = graphql_mutation_response(:create_custom_emoji) + expect(gql_response['errors']).to eq([]) + expect(gql_response['customEmoji']['name']).to eq(attributes[:name]) + expect(gql_response['customEmoji']['url']).to eq(attributes[:url]) + end + end +end