Commit aefa6568 authored by Brett Walker's avatar Brett Walker Committed by Nick Thomas

Add support for querying epics with GraphQL

wrapped into a Group query
parent 1a23e034
......@@ -10,7 +10,7 @@ module Resolvers
end
end
def self.resolver_complexity(args)
def self.resolver_complexity(args, child_complexity:)
complexity = 1
complexity += 1 if args[:sort]
complexity += 5 if args[:search]
......
......@@ -20,7 +20,7 @@ module ResolvesPipelines
end
class_methods do
def resolver_complexity(args)
def resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:sha]
complexity += 2 if args[:ref]
......
......@@ -58,7 +58,7 @@ module Resolvers
IssuesFinder.new(context[:current_user], args).execute
end
def self.resolver_complexity(args)
def self.resolver_complexity(args, child_complexity:)
complexity = super
complexity += 2 if args[:labelName]
......
......@@ -33,7 +33,7 @@ module Types
limit_value = [args[:first], args[:last], page_size].compact.min
# Resolvers may add extra complexity depending on used arguments
complexity = child_complexity + self.resolver&.try(:resolver_complexity, args).to_i
complexity = child_complexity + self.resolver&.try(:resolver_complexity, args, child_complexity: child_complexity).to_i
# Resolvers may add extra complexity depending on number of items being loaded.
multiplier = self.resolver&.try(:complexity_multiplier, args).to_f
......
......@@ -21,3 +21,5 @@ module Types
end
end
end
Types::GroupType.prepend(EE::Types::GroupType)
......@@ -15,6 +15,10 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true
field :state, IssueStateEnum, null: false
field :reference, GraphQL::STRING_TYPE, null: false, method: :to_reference do
argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false
end
field :author, Types::UserType,
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
......@@ -37,7 +41,9 @@ module Types
field :upvotes, GraphQL::INT_TYPE, null: false
field :downvotes, GraphQL::INT_TYPE, null: false
field :user_notes_count, GraphQL::INT_TYPE, null: false
field :web_path, GraphQL::STRING_TYPE, null: false, method: :issue_path
field :web_url, GraphQL::STRING_TYPE, null: false
field :relative_position, GraphQL::INT_TYPE, null: true
field :closed_at, Types::TimeType, null: true
......
......@@ -4,6 +4,16 @@ class IssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
def web_url
Gitlab::UrlBuilder.build(issue)
url_builder.url
end
def issue_path
url_builder.issue_path(issue)
end
private
def url_builder
@url_builder ||= Gitlab::UrlBuilder.new(issue)
end
end
# frozen_string_literal: true
module EE
module Types
module GroupType
extend ActiveSupport::Concern
prepended do
%i[epics].each do |feature|
field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (group, args, ctx) do
group.feature_available?(feature)
end
end
field :epic,
::Types::EpicType,
null: true,
resolver: ::Resolvers::EpicResolver.single
field :epics,
::Types::EpicType.connection_type,
null: true,
resolver: ::Resolvers::EpicResolver
end
end
end
end
......@@ -6,6 +6,10 @@ module EE
extend ActiveSupport::Concern
prepended do
field :weight, GraphQL::INT_TYPE,
null: true,
resolve: -> (obj, _args, _ctx) { obj.supports_weight? ? obj.weight : nil }
field :designs, ::Types::DesignManagement::DesignCollectionType,
null: true, method: :design_collection
end
......
# frozen_string_literal: true
module Resolvers
class EpicIssuesResolver < BaseResolver
type Types::EpicIssueType, null: true
alias_method :epic, :object
def resolve(**args)
epic.issues_readable_by(context[:current_user])
end
end
end
# frozen_string_literal: true
module Resolvers
class EpicResolver < BaseResolver
argument :iid, GraphQL::ID_TYPE,
required: false,
description: 'The IID of the epic, e.g., "1"'
argument :iids, [GraphQL::ID_TYPE],
required: false,
description: 'The list of IIDs of epics, e.g., [1, 2]'
type Types::EpicType, null: true
def resolve(**args)
return [] unless object.present?
return [] unless epic_feature_enabled?
find_epics(transform_args(args))
end
private
def find_epics(args)
EpicsFinder.new(context[:current_user], args).execute
end
def epic_feature_enabled?
group.feature_available?(:epics)
end
def transform_args(args)
transformed = args.dup
transformed[:group_id] = group.id
transformed[:parent_id] = parent.id if parent
transformed[:iids] ||= [args[:iid]].compact
transformed
end
# `object` refers to the object we're currently querying on, and is usually a `Group`
# when querying an Epic. In the case of field that uses this resolver, for example
# an Epic's `children` field, then `object` is an `EpicPresenter` (rather than an Epic).
# But that's the epic we need in order to scope the find to only children of this epic,
# using the `parent_id`
def parent
object if object.is_a?(EpicPresenter)
end
def group
return object if object.is_a?(Group)
parent.group
end
# If we're querying for multiple iids and selecting issues, then ideally
# we want to batch the epic and issue queries into one to reduce N+1 and memory.
# https://gitlab.com/gitlab-org/gitlab-ee/issues/11841
# Until we do that, add in child_complexity for each iid requested
# (minus one for the automatically added child_complexity in the BaseField)
def self.resolver_complexity(args, child_complexity:)
complexity = super
complexity += (args[:iids].count - 1) * child_complexity if args[:iids]
complexity
end
end
end
# frozen_string_literal: true
module Types
class EpicIssueType < IssueType
graphql_name 'EpicIssue'
present_using EpicIssuePresenter
field :epic_issue_id, GraphQL::ID_TYPE, null: false
field :relation_path, GraphQL::STRING_TYPE, null: true, resolve: -> (issue, args, ctx) do
issue.group_epic_issue_path(ctx[:current_user])
end
end
end
# frozen_string_literal: true
module Types
class EpicStateEnum < BaseEnum
graphql_name 'EpicState'
description 'State of a GitLab epic'
value 'opened'
value 'closed'
end
end
# frozen_string_literal: true
module Types
class EpicType < BaseObject
graphql_name 'Epic'
authorize :read_epic
expose_permissions Types::PermissionTypes::Epic
present_using EpicPresenter
field :id, GraphQL::ID_TYPE, null: false
field :iid, GraphQL::ID_TYPE, null: false
field :title, GraphQL::STRING_TYPE, null: true
field :description, GraphQL::STRING_TYPE, null: true
field :state, EpicStateEnum, null: false
field :group, GroupType,
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.group_id).find }
field :parent, EpicType,
null: true,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Epic, obj.parent_id).find }
field :author, Types::UserType,
null: false,
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, obj.author_id).find }
field :start_date, Types::TimeType, null: true
field :start_date_is_fixed, GraphQL::BOOLEAN_TYPE, null: true, method: :start_date_is_fixed?, authorize: :admin_epic
field :start_date_fixed, Types::TimeType, null: true, authorize: :admin_epic
field :start_date_from_milestones, Types::TimeType, null: true, authorize: :admin_epic
field :due_date, Types::TimeType, null: true
field :due_date_is_fixed, GraphQL::BOOLEAN_TYPE, null: true, method: :due_date_is_fixed?, authorize: :admin_epic
field :due_date_fixed, Types::TimeType, null: true, authorize: :admin_epic
field :due_date_from_milestones, Types::TimeType, null: true, authorize: :admin_epic
field :closed_at, Types::TimeType, null: true
field :created_at, Types::TimeType, null: true
field :updated_at, Types::TimeType, null: true
field :children,
::Types::EpicType.connection_type,
null: true,
resolver: ::Resolvers::EpicResolver
field :has_children, GraphQL::BOOLEAN_TYPE, null: false, method: :has_children?
field :has_issues, GraphQL::BOOLEAN_TYPE, null: false, method: :has_issues?
field :web_path, GraphQL::STRING_TYPE, null: false, method: :group_epic_path
field :web_url, GraphQL::STRING_TYPE, null: false, method: :group_epic_url
field :relation_path, GraphQL::STRING_TYPE, null: true, method: :group_epic_link_path
field :reference, GraphQL::STRING_TYPE, null: false, method: :epic_reference do
argument :full, GraphQL::BOOLEAN_TYPE, required: false, default_value: false
end
field :issues,
Types::EpicIssueType.connection_type,
null: true,
resolver: Resolvers::EpicIssuesResolver
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class Epic < BasePermissionType
description 'Check permissions for the current user on an epic'
graphql_name 'EpicPermissions'
abilities :read_epic, :read_epic_iid, :update_epic, :destroy_epic, :admin_epic,
:create_epic, :create_note, :award_emoji
end
end
end
# frozen_string_literal: true
class EpicIssuePresenter < Gitlab::View::Presenter::Delegated
presents :issue
def group_epic_issue_path(current_user)
return unless can_admin_issue_link?(current_user)
url_builder.group_epic_issue_path(issue.epic.group, issue.epic.iid, issue.epic_issue_id)
end
private
def url_builder
@url_builder ||= Gitlab::UrlBuilder.new(issue)
end
def can_admin_issue_link?(current_user)
Ability.allowed?(current_user, :admin_epic_issue, issue) && Ability.allowed?(current_user, :admin_epic, issue.epic)
end
end
......@@ -13,6 +13,28 @@ class EpicPresenter < Gitlab::View::Presenter::Delegated
}
end
def group_epic_path
url_builder.group_epic_path(epic.group, epic)
end
def group_epic_url
url_builder.group_epic_url(epic.group, epic)
end
def group_epic_link_path
return unless epic.parent
url_builder.group_epic_link_path(epic.group, epic.parent.iid, epic.id)
end
def epic_reference(full: false)
if full
epic.to_reference(full: true)
else
epic.to_reference(epic.parent&.group || epic.group)
end
end
private
def initial_data
......@@ -111,11 +133,16 @@ class EpicPresenter < Gitlab::View::Presenter::Delegated
{
id: epic.id,
title: epic.title,
url: epic_path(epic),
url: group_epic_path,
state: epic.state,
human_readable_end_date: epic.end_date&.to_s(:medium),
human_readable_timestamp: remaining_days_in_words(epic.end_date, epic.start_date)
}
end
end
# important for using routing helpers in GraphQL
def url_builder
@url_builder ||= Gitlab::UrlBuilder.new(epic)
end
end
---
title: Add support for querying epics with GraphQL
merge_request: 13248
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Group'] do
describe 'nested epic request' do
it { expect(described_class).to have_graphql_field(:epicsEnabled) }
it { expect(described_class).to have_graphql_field(:epics) }
it { expect(described_class).to have_graphql_field(:epic) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::EpicResolver do
include GraphqlHelpers
set(:current_user) { create(:user) }
set(:user2) { create(:user) }
context "with a group" do
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let(:epic1) { create(:epic, group: group, state: :closed, created_at: 3.days.ago, updated_at: 2.days.ago) }
let(:epic2) { create(:epic, group: group, author: user2, title: 'foo', description: 'bar', created_at: 2.days.ago, updated_at: 3.days.ago) }
before do
group.add_developer(current_user)
stub_licensed_features(epics: true)
end
describe '#resolve' do
it 'returns nothing when feature disabled' do
stub_licensed_features(epics: false)
expect(resolve_epics).to be_empty
end
it 'finds all epics' do
expect(resolve_epics).to contain_exactly(epic1, epic2)
end
context 'with iid' do
it 'finds a specific epic with iid' do
expect(resolve_epics(iid: epic1.iid)).to contain_exactly(epic1)
end
it 'does not inflate the complexity' do
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
expect(field.to_graphql.complexity.call({}, { iid: [epic1.iid] }, 5)).to eq 6
end
end
context 'with iids' do
it 'finds a specific epic with iids' do
expect(resolve_epics(iids: epic1.iid)).to contain_exactly(epic1)
end
it 'finds multiple epics with iids' do
expect(resolve_epics(iids: [epic1.iid, epic2.iid]))
.to contain_exactly(epic1, epic2)
end
it 'increases the complexity based on child_complexity and number of iids' do
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
expect(field.to_graphql.complexity.call({}, { iids: [epic1.iid] }, 5)).to eq 6
expect(field.to_graphql.complexity.call({}, { iids: [epic1.iid, epic2.iid] }, 5)).to eq 11
end
end
context 'with subgroups' do
let(:sub_group) { create(:group, parent: group) }
let(:iids) { [epic1, epic2].map(&:iid) }
let!(:epic3) { create(:epic, group: sub_group, iid: epic1.iid) }
let!(:epic4) { create(:epic, group: sub_group, iid: epic2.iid) }
before do
sub_group.add_developer(current_user)
end
it 'finds only the epics within the group we are looking at' do
expect(resolve_epics(iids: iids)).to contain_exactly(epic1, epic2)
end
it 'return all epics' do
expect(resolve_epics).to contain_exactly(epic1, epic2, epic3, epic4)
end
end
end
end
context "when passing a non existent, batch loaded group" do
let(:group) do
BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
loader.call("non-existent-path", nil)
end
end
it "returns nil without breaking" do
expect(resolve_epics(iids: ["don't", "break"])).to be_empty
end
end
def resolve_epics(args = {}, context = { current_user: current_user })
resolve(described_class, obj: group, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['EpicIssue'] do
it { expect(described_class.graphql_name).to eq('EpicIssue') }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
it 'has specific fields' do
%i[epic_issue_id relation_path].each do |field_name|
expect(described_class).to have_graphql_field(field_name)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['EpicState'] do
it { expect(described_class.graphql_name).to eq('EpicState') }
it 'exposes all the existing epic states' do
expect(described_class.values.keys).to include(*%w[opened closed])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['Epic'] do
let(:fields) do
%i[
id iid title description state group parent author
start_date start_date_is_fixed start_date_fixed start_date_from_milestones
due_date due_date_is_fixed due_date_fixed due_date_from_milestones
closed_at created_at updated_at children has_children has_issues
web_path web_url relation_path reference issues
user_permissions
]
end
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Epic) }
it { expect(described_class.graphql_name).to eq('Epic') }
it { expect(described_class).to require_graphql_authorizations(:read_epic) }
it { expect(described_class).to have_graphql_fields(fields) }
end
......@@ -3,5 +3,7 @@
require 'spec_helper'
describe GitlabSchema.types['Issue'] do
it { expect(described_class).to have_graphql_field(:weight) }
it { expect(described_class).to have_graphql_field(:designs) }
end
# frozen_string_literal: true
require 'spec_helper'
describe Types::PermissionTypes::Epic do
it do
expected_permissions = [:read_epic, :read_epic_iid, :update_epic, :destroy_epic,
:admin_epic, :create_epic, :create_note, :award_emoji]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe EpicIssuePresenter do
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:epic) { create(:epic, group: group)}
let(:issue) { create(:issue, project: project) }
let!(:epic_issue) { create(:epic_issue, issue: issue, epic: epic) }
let(:target_issue) { epic.issues_readable_by(user).first }
let(:presenter) { described_class.new(target_issue, current_user: user) }
before do
stub_licensed_features(epics: true)
group.add_developer(user)
end
describe '#group_epic_issue_path' do
it 'returns correct path' do
expect(presenter.group_epic_issue_path(user)).to eq "/groups/#{group.name}/-/epics/#{epic.iid}/issues/#{target_issue.epic_issue_id}"
end
it 'returns nil without proper permission' do
unauth_user = create(:user)
expect(presenter.group_epic_issue_path(unauth_user)).to be_nil
end
end
end
......@@ -4,30 +4,24 @@ require 'spec_helper'
describe EpicPresenter do
include UsersHelper
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:parent_epic) { create(:epic, group: group, start_date: Date.new(2000, 1, 10), due_date: Date.new(2000, 1, 20)) }
let(:epic) { create(:epic, group: group, author: user, parent: parent_epic) }
let(:presenter) { described_class.new(epic, current_user: user) }
describe '#show_data' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:milestone1) { create(:milestone, title: 'make me a sandwich', start_date: '2010-01-01', due_date: '2019-12-31') }
let(:milestone2) { create(:milestone, title: 'make me a pizza', start_date: '2020-01-01', due_date: '2029-12-31') }
let(:parent_epic) { create(:epic, group: group, start_date: Date.new(2000, 1, 10), due_date: Date.new(2000, 1, 20)) }
let(:epic) do
create(
:epic,
group: group,
author: user,
start_date_sourcing_milestone: milestone1,
start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone2,
due_date: Date.new(2000, 1, 2),
parent: parent_epic
)
end
let(:presenter) { described_class.new(epic, current_user: user) }
before do
epic.update(
start_date_sourcing_milestone: milestone1, start_date: Date.new(2000, 1, 1),
due_date_sourcing_milestone: milestone2, due_date: Date.new(2000, 1, 2)
)
stub_licensed_features(epics: true)
end
......@@ -59,4 +53,32 @@ describe EpicPresenter do
expect { presenter.show_data }.not_to exceed_query_limit(control_count)
end
end
describe '#group_epic_path' do
it 'returns correct path' do
expect(presenter.group_epic_path).to eq group_epic_path(epic.group, epic)
end
end
describe '#group_epic_link_path' do
it 'returns correct path' do
expect(presenter.group_epic_link_path).to eq group_epic_link_path(epic.group, epic.parent.iid, epic.id)
end
it 'returns nothing with nil parent' do
epic.parent = nil
expect(presenter.group_epic_link_path).to be_nil
end
end
describe '#epic_reference' do
it 'returns a reference' do
expect(presenter.epic_reference).to eq "&#{epic.iid}"
end
it 'returns a full reference' do
expect(presenter.epic_reference(full: true)).to eq "#{epic.parent.group.name}&#{epic.iid}"
end
end
end
# frozen_string_literal: true
require 'spec_helper'
# Based on ee/spec/requests/api/epics_spec.rb
# Should follow closely in order to ensure all situations are covered
describe 'Epics through GroupQuery' do
include GraphqlHelpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, group: group) }
let(:label) { create(:label) }
let(:epic) { create(:labeled_epic, group: group, labels: [label]) }
let(:epics_data) { graphql_data['group']['epics']['edges'] }
let(:epic_data) { graphql_data['group']['epic'] }
# similar to GET /groups/:id/epics
describe 'Get list of epics from a group' do
let(:query) do
epic_node = <<~NODE
edges {
node {
id
iid
title
userPermissions {
adminEpic
}
}
}
NODE
graphql_query_for("group", { "fullPath" => group.full_path },
['epicsEnabled',
query_graphql_field("epics", {}, epic_node)]
)
end
context 'when the request is correct' do
before do
stub_licensed_features(epics: true)
epic && group.reload
post_graphql(query, current_user: user)
end
it_behaves_like 'a working graphql query'
it 'returns epics successfully' do
expect(response).to have_gitlab_http_status(200)
expect(graphql_errors).to be_nil
expect(node_array('id').first).to eq epic.id.to_s
expect(graphql_data['group']['epicsEnabled']).to be_truthy
end
end
context 'with multiple epics' do
let(:user2) { create(:user) }
let!(:epic) { create(:epic, group: group, state: :closed, created_at: 3.days.ago, updated_at: 2.days.ago) }
let!(:epic2) { create(:epic, author: user2, group: group, title: 'foo', description: 'bar', created_at: 2.days.ago, updated_at: 3.days.ago) }
before do
stub_licensed_features(epics: true)
end
it 'sorts by created_at descending by default' do
post_graphql(query, current_user: user)
expect_array_response([epic2.id, epic.id])
end
describe 'can admin epics' do
context 'when permission is absent' do
it 'returns false for adminEpic' do
post_graphql(query, current_user: user)
expect(node_array('userPermissions')).to all(include('adminEpic' => false))
end
end
context 'when permission is present' do
before do
group.add_maintainer(user)
end
it 'returns true for adminEpic' do
post_graphql(query, current_user: user)
expect(node_array('userPermissions')).to all(include('adminEpic' => true))
end
end
end
end
context 'when error requests' do
context 'when epics feature is disabled' do
it 'returns empty' do
group.add_developer(user)
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to be_nil
expect(epics_data).to be_empty
expect(graphql_data['group']['epicsEnabled']).to be_falsey
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
it 'returns a nil group for a user without permissions to see the group' do
project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
group.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
post_graphql(query, current_user: user)
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to be_nil
expect(graphql_data['group']).to be_nil
end
end
end
end
end
# similar to 'GET /groups/:id/epics/:epic_iid'
describe 'Get epic from a group' do
let(:query) do
graphql_query_for('group', { 'fullPath' => group.full_path },
['epicsEnabled',
query_graphql_field('epic', { iid: epic.iid })]
)
end
context 'when the request is correct' do
before do
stub_licensed_features(epics: true)
post_graphql(query)
end
it_behaves_like 'a working graphql query'
it 'returns an epic successfully' do
expect(response).to have_gitlab_http_status(:success)
expect(graphql_errors).to be_nil
expect(epic_data['id']).to eq epic.id.to_s
expect(graphql_data['group']['epicsEnabled']).to be_truthy
end
end
end
def expect_array_response(items)
expect(response).to have_gitlab_http_status(:success)
expect(epics_data).to be_an Array
expect(node_array('id').map(&:to_i)).to eq(Array(items))
end
def node_array(extract_attribute = nil)
epics_data.map do |item|
extract_attribute ? item['node'][extract_attribute] : item['node']
end
end
end
......@@ -6,7 +6,7 @@ describe Types::BaseField do
context 'when considering complexity' do
let(:resolver) do
Class.new(described_class) do
def self.resolver_complexity(args)
def self.resolver_complexity(args, child_complexity:)
2 if args[:foo]
end
......
......@@ -6,4 +6,10 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class.graphql_name).to eq('Issue') }
it { expect(described_class).to require_graphql_authorizations(:read_issue) }
it 'has specific fields' do
%i[relative_position web_path web_url reference].each do |field_name|
expect(described_class).to have_graphql_field(field_name)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe IssuePresenter do
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
let(:presenter) { described_class.new(issue, current_user: user) }
before do
group.add_developer(user)
end
describe '#web_url' do
it 'returns correct path' do
expect(presenter.web_url).to eq "http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}"
end
end
describe '#issue_path' do
it 'returns correct path' do
expect(presenter.issue_path).to eq "/#{group.name}/#{project.name}/issues/#{issue.iid}"
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment