Commit a4c1dbf7 authored by Markus Koller's avatar Markus Koller Committed by Rémy Coutable

Support Git access for group wikis

This adds all the necessary routes and other behaviour to enable local
Git access to group wiki repositories.

To handle lookups of the wiki container in `Gitlab::RepoPath`, we
introduce a `Routable.find_by_full_path` helper to find routable objects
without having to know their class.
parent 6494c6b1
...@@ -186,6 +186,10 @@ module WikiActions ...@@ -186,6 +186,10 @@ module WikiActions
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
def git_access
render 'shared/wikis/git_access'
end
private private
def container def container
......
...@@ -6,7 +6,4 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -6,7 +6,4 @@ class Projects::WikisController < Projects::ApplicationController
alias_method :container, :project alias_method :container, :project
feature_category :wiki feature_category :wiki
def git_access
end
end end
...@@ -11,12 +11,14 @@ module CaseSensitivity ...@@ -11,12 +11,14 @@ module CaseSensitivity
def iwhere(params) def iwhere(params)
criteria = self criteria = self
params.each do |key, value| params.each do |column, value|
column = arel_table[column] unless column.is_a?(Arel::Attribute)
criteria = case value criteria = case value
when Array when Array
criteria.where(value_in(key, value)) criteria.where(value_in(column, value))
else else
criteria.where(value_equal(key, value)) criteria.where(value_equal(column, value))
end end
end end
...@@ -28,7 +30,7 @@ module CaseSensitivity ...@@ -28,7 +30,7 @@ module CaseSensitivity
def value_equal(column, value) def value_equal(column, value)
lower_value = lower_value(value) lower_value = lower_value(value)
lower_column(arel_table[column]).eq(lower_value).to_sql lower_column(column).eq(lower_value).to_sql
end end
def value_in(column, values) def value_in(column, values)
...@@ -36,7 +38,7 @@ module CaseSensitivity ...@@ -36,7 +38,7 @@ module CaseSensitivity
lower_value(value) lower_value(value)
end end
lower_column(arel_table[column]).in(lower_values).to_sql lower_column(column).in(lower_values).to_sql
end end
def lower_value(value) def lower_value(value)
......
...@@ -4,6 +4,36 @@ ...@@ -4,6 +4,36 @@
# Object must have name and path db fields and respond to parent and parent_changed? methods. # Object must have name and path db fields and respond to parent and parent_changed? methods.
module Routable module Routable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include CaseSensitivity
# Finds a Routable object by its full path, without knowing the class.
#
# Usage:
#
# Routable.find_by_full_path('groupname') # -> Group
# Routable.find_by_full_path('groupname/projectname') # -> Project
#
# Returns a single object, or nil.
def self.find_by_full_path(path, follow_redirects: false, route_scope: Route, redirect_route_scope: RedirectRoute)
return unless path.present?
# Case sensitive match first (it's cheaper and the usual case)
# If we didn't have an exact match, we perform a case insensitive search
#
# We need to qualify the columns with the table name, to support both direct lookups on
# Route/RedirectRoute, and scoped lookups through the Routable classes.
route =
route_scope.find_by(routes: { path: path }) ||
route_scope.iwhere(Route.arel_table[:path] => path).take
if follow_redirects
route ||= redirect_route_scope.iwhere(RedirectRoute.arel_table[:path] => path).take
end
return unless route
route.is_a?(Routable) ? route : route.source
end
included do included do
# Remove `inverse_of: source` when upgraded to rails 5.2 # Remove `inverse_of: source` when upgraded to rails 5.2
...@@ -30,15 +60,14 @@ module Routable ...@@ -30,15 +60,14 @@ module Routable
# #
# Returns a single object, or nil. # Returns a single object, or nil.
def find_by_full_path(path, follow_redirects: false) def find_by_full_path(path, follow_redirects: false)
# Case sensitive match first (it's cheaper and the usual case) # TODO: Optimize these queries by avoiding joins
# If we didn't have an exact match, we perform a case insensitive search # https://gitlab.com/gitlab-org/gitlab/-/issues/292252
found = includes(:route).find_by(routes: { path: path }) || where_full_path_in([path]).take Routable.find_by_full_path(
path,
return found if found follow_redirects: follow_redirects,
route_scope: includes(:route).references(:routes),
if follow_redirects redirect_route_scope: joins(:redirect_routes)
joins(:redirect_routes).find_by("LOWER(redirect_routes.path) = LOWER(?)", path) )
end
end end
# Builds a relation to find multiple objects by their full paths. # Builds a relation to find multiple objects by their full paths.
......
# frozen_string_literal: true # frozen_string_literal: true
class RedirectRoute < ApplicationRecord class RedirectRoute < ApplicationRecord
include CaseSensitivity
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true validates :source, presence: true
......
...@@ -4,7 +4,7 @@ class Route < ApplicationRecord ...@@ -4,7 +4,7 @@ class Route < ApplicationRecord
include CaseSensitivity include CaseSensitivity
include Gitlab::SQL::Pattern include Gitlab::SQL::Pattern
belongs_to :source, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :source, polymorphic: true, inverse_of: :route # rubocop:disable Cop/PolymorphicAssociations
validates :source, presence: true validates :source, presence: true
validates :path, validates :path,
......
...@@ -4,11 +4,10 @@ ...@@ -4,11 +4,10 @@
%a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" } %a.gutter-toggle.float-right.d-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" }
= sprite_icon('chevron-double-lg-right', css_class: 'gl-icon') = sprite_icon('chevron-double-lg-right', css_class: 'gl-icon')
- if @wiki.container.is_a?(Project) - git_access_url = wiki_path(@wiki, action: :git_access)
- git_access_url = wiki_path(@wiki, action: :git_access) = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do
= link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '', data: { qa_selector: 'clone_repository_link' } do = sprite_icon('download', css_class: 'gl-mr-2')
= sprite_icon('download', css_class: 'gl-mr-2') %span= _("Clone repository")
%span= _("Clone repository")
- if @sidebar_error.present? - if @sidebar_error.present?
= render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.') = render 'shared/alert_info', body: s_('Wiki|The sidebar failed to load. You can reload the page to try again.')
......
---
title: Support Git access for group wikis
merge_request: 45892
author:
type: added
...@@ -9,7 +9,7 @@ scope(path: '*repository_path', format: false) do ...@@ -9,7 +9,7 @@ scope(path: '*repository_path', format: false) do
end end
# NOTE: LFS routes are exposed on all repository types, but we still check for # NOTE: LFS routes are exposed on all repository types, but we still check for
# LFS availability on the repository container in LfsRequest#require_lfs_enabled! # LFS availability on the repository container in LfsRequest#lfs_check_access!
# Git LFS API (metadata) # Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do scope(path: 'info/lfs/objects', controller: :lfs_api) do
......
...@@ -404,7 +404,7 @@ and above. ...@@ -404,7 +404,7 @@ and above.
There are a few limitations compared to project wikis: There are a few limitations compared to project wikis:
- Local Git access is not supported yet. - Git LFS is not supported.
- Group wikis are not included in global search, group exports, backups, and Geo replication. - Group wikis are not included in global search, group exports, backups, and Geo replication.
- Changes to group wikis don't show up in the group's activity feed. - Changes to group wikis don't show up in the group's activity feed.
- Group wikis [can't be moved](../../api/project_repository_storage_moves.md#limitations) using the project - Group wikis [can't be moved](../../api/project_repository_storage_moves.md#limitations) using the project
......
import initWikis from '~/pages/shared/wikis'; import initWikis from '~/pages/shared/wikis';
import initClonePanel from '~/clone_panel';
initWikis(); initWikis();
initClonePanel();
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Repositories::GitHttpController do
context 'when repository container is a group wiki' do
include WikiHelpers
let_it_be(:group) { create(:group, :wiki_repo) }
let_it_be(:user) { create(:user) }
before_all do
group.add_owner(user)
end
before do
stub_group_wikis(true)
end
it_behaves_like Repositories::GitHttpController do
let(:container) { group.wiki }
let(:access_checker_class) { Gitlab::GitAccessWiki }
end
end
end
...@@ -23,4 +23,5 @@ RSpec.describe 'Group wikis' do ...@@ -23,4 +23,5 @@ RSpec.describe 'Group wikis' do
it_behaves_like 'User views a wiki page' it_behaves_like 'User views a wiki page'
it_behaves_like 'User views wiki pages' it_behaves_like 'User views wiki pages'
it_behaves_like 'User views wiki sidebar' it_behaves_like 'User views wiki sidebar'
it_behaves_like 'User views Git access wiki page'
end end
...@@ -21,6 +21,27 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -21,6 +21,27 @@ RSpec.describe 'Git LFS API and storage' do
let(:sample_oid) { lfs_object.oid } let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size } let(:sample_size) { lfs_object.size }
context 'with group wikis' do
let_it_be(:group) { create(:group) }
# LFS is not supported on group wikis, so we override the shared examples
# to expect 404 responses instead.
[
'LFS http 200 response',
'LFS http 200 blob response',
'LFS http 403 response'
].each do |examples|
shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' }
end
it_behaves_like 'LFS http requests' do
let(:container) { create(:group_wiki, :empty_repo, group: group) }
let(:authorize_guest) { group.add_guest(user) }
let(:authorize_download) { group.add_reporter(user) }
let(:authorize_upload) { group.add_developer(user) }
end
end
describe 'when handling lfs batch request' do describe 'when handling lfs batch request' do
let(:update_lfs_permissions) { } let(:update_lfs_permissions) { }
let(:update_user_permissions) { } let(:update_user_permissions) { }
......
...@@ -131,6 +131,24 @@ RSpec.describe Repositories::GitHttpController, type: :request do ...@@ -131,6 +131,24 @@ RSpec.describe Repositories::GitHttpController, type: :request do
it_behaves_like 'triggers Geo' it_behaves_like 'triggers Geo'
end end
context 'with a group wiki' do
include WikiHelpers
let_it_be(:group) { create(:group, :wiki_repo) }
let_it_be(:user) { create(:user) }
let(:path) { "#{group.wiki.full_path}.git" }
before_all do
group.add_owner(user)
end
before do
stub_group_wikis(true)
end
it_behaves_like 'triggers Geo'
end
context 'with a personal snippet' do context 'with a personal snippet' do
let_it_be(:snippet) { create(:personal_snippet, :repository, author: user) } let_it_be(:snippet) { create(:personal_snippet, :repository, author: user) }
let_it_be(:path) { "snippets/#{snippet.id}.git" } let_it_be(:path) { "snippets/#{snippet.id}.git" }
......
...@@ -20,6 +20,7 @@ module Gitlab ...@@ -20,6 +20,7 @@ module Gitlab
end, end,
container_class: ProjectWiki, container_class: ProjectWiki,
project_resolver: -> (wiki) { wiki.try(:project) }, project_resolver: -> (wiki) { wiki.try(:project) },
guest_read_ability: :download_wiki_code,
suffix: :wiki suffix: :wiki
).freeze ).freeze
SNIPPET = RepoType.new( SNIPPET = RepoType.new(
......
...@@ -4,6 +4,13 @@ module Gitlab ...@@ -4,6 +4,13 @@ module Gitlab
module RepoPath module RepoPath
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
# Returns an array containing:
# - The repository container
# - The related project (if available)
# - The repository type
# - The original container path (if redirected)
#
# @returns [HasRepository, Project, String, String]
def self.parse(path) def self.parse(path)
repo_path = path.delete_prefix('/').delete_suffix('.git') repo_path = path.delete_prefix('/').delete_suffix('.git')
redirected_path = nil redirected_path = nil
...@@ -30,7 +37,15 @@ module Gitlab ...@@ -30,7 +37,15 @@ module Gitlab
[nil, nil, Gitlab::GlRepository.default_type, nil] [nil, nil, Gitlab::GlRepository.default_type, nil]
end end
# Returns an array containing:
# - The repository container
# - The related project (if available)
# - The original container path (if redirected)
#
# @returns [HasRepository, Project, String]
def self.find_container(type, full_path) def self.find_container(type, full_path)
return [nil, nil, nil] if full_path.blank?
if type.snippet? if type.snippet?
snippet, redirected_path = find_snippet(full_path) snippet, redirected_path = find_snippet(full_path)
...@@ -47,26 +62,24 @@ module Gitlab ...@@ -47,26 +62,24 @@ module Gitlab
end end
def self.find_project(project_path) def self.find_project(project_path)
return [nil, nil] if project_path.blank?
project = Project.find_by_full_path(project_path, follow_redirects: true) project = Project.find_by_full_path(project_path, follow_redirects: true)
redirected_path = redirected?(project, project_path) ? project_path : nil redirected_path = project_path if redirected?(project, project_path)
[project, redirected_path] [project, redirected_path]
end end
def self.redirected?(project, project_path) def self.redirected?(container, container_path)
project && project.full_path.casecmp(project_path) != 0 container && container.full_path.casecmp(container_path) != 0
end end
# Snippet_path can be either: # Snippet_path can be either:
# - snippets/1 # - snippets/1
# - h5bp/html5-boilerplate/snippets/53 # - h5bp/html5-boilerplate/snippets/53
def self.find_snippet(snippet_path) def self.find_snippet(snippet_path)
return [nil, nil] if snippet_path.blank?
snippet_id, project_path = extract_snippet_info(snippet_path) snippet_id, project_path = extract_snippet_info(snippet_path)
project, redirected_path = find_project(project_path) return [nil, nil] unless snippet_id
project, redirected_path = find_project(project_path) if project_path
[Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path] [Snippet.find_by_id_and_project(id: snippet_id, project: project), redirected_path]
end end
...@@ -74,19 +87,23 @@ module Gitlab ...@@ -74,19 +87,23 @@ module Gitlab
# Wiki path can be either: # Wiki path can be either:
# - namespace/project # - namespace/project
# - group/subgroup/project # - group/subgroup/project
def self.find_wiki(wiki_path) #
return [nil, nil] if wiki_path.blank? # And also in EE:
# - group
project, redirected_path = find_project(wiki_path) # - group/subgroup
def self.find_wiki(container_path)
[project&.wiki, redirected_path] container = Routable.find_by_full_path(container_path, follow_redirects: true)
redirected_path = container_path if redirected?(container, container_path)
# In CE, Group#wiki is not available so this will return nil for a group path.
[container&.try(:wiki), redirected_path]
end end
def self.extract_snippet_info(snippet_path) def self.extract_snippet_info(snippet_path)
path_segments = snippet_path.split('/') path_segments = snippet_path.split('/')
snippet_id = path_segments.pop snippet_id = path_segments.pop
path_segments.pop # Remove snippets from path path_segments.pop # Remove 'snippets' from path
project_path = File.join(path_segments) project_path = File.join(path_segments).presence
[snippet_id, project_path] [snippet_id, project_path]
end end
......
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Repositories::GitHttpController do RSpec.describe Repositories::GitHttpController do
include GitHttpHelpers
let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) } let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) } let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
......
...@@ -4,16 +4,16 @@ require 'spec_helper' ...@@ -4,16 +4,16 @@ require 'spec_helper'
RSpec.describe CaseSensitivity do RSpec.describe CaseSensitivity do
describe '.iwhere' do describe '.iwhere' do
let(:connection) { ActiveRecord::Base.connection } let_it_be(:connection) { ActiveRecord::Base.connection }
let(:model) do let_it_be(:model) do
Class.new(ActiveRecord::Base) do Class.new(ActiveRecord::Base) do
include CaseSensitivity include CaseSensitivity
self.table_name = 'namespaces' self.table_name = 'namespaces'
end end
end end
let!(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') } let_it_be(:model_1) { model.create!(path: 'mOdEl-1', name: 'mOdEl 1') }
let!(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') } let_it_be(:model_2) { model.create!(path: 'mOdEl-2', name: 'mOdEl 2') }
it 'finds a single instance by a single attribute regardless of case' do it 'finds a single instance by a single attribute regardless of case' do
expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1) expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1)
...@@ -28,6 +28,10 @@ RSpec.describe CaseSensitivity do ...@@ -28,6 +28,10 @@ RSpec.describe CaseSensitivity do
.to contain_exactly(model_1) .to contain_exactly(model_1)
end end
it 'finds instances by custom Arel attributes' do
expect(model.iwhere(model.arel_table[:path] => 'MODEL-1')).to contain_exactly(model_1)
end
it 'builds a query using LOWER' do it 'builds a query using LOWER' do
query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql
expected_query = <<~QRY.strip expected_query = <<~QRY.strip
......
...@@ -2,8 +2,69 @@ ...@@ -2,8 +2,69 @@
require 'spec_helper' require 'spec_helper'
RSpec.shared_examples '.find_by_full_path' do
describe '.find_by_full_path', :aggregate_failures do
it 'finds records by their full path' do
expect(described_class.find_by_full_path(record.full_path)).to eq(record)
expect(described_class.find_by_full_path(record.full_path.upcase)).to eq(record)
end
it 'returns nil for unknown paths' do
expect(described_class.find_by_full_path('unknown')).to be_nil
end
it 'includes route information when loading a record' do
control_count = ActiveRecord::QueryRecorder.new do
described_class.find_by_full_path(record.full_path)
end.count
expect do
described_class.find_by_full_path(record.full_path).route
end.not_to exceed_all_query_limit(control_count)
end
context 'with redirect routes' do
let_it_be(:redirect_route) { create(:redirect_route, source: record) }
context 'without follow_redirects option' do
it 'does not find records by their redirected path' do
expect(described_class.find_by_full_path(redirect_route.path)).to be_nil
expect(described_class.find_by_full_path(redirect_route.path.upcase)).to be_nil
end
end
context 'with follow_redirects option set to true' do
it 'finds records by their canonical path' do
expect(described_class.find_by_full_path(record.full_path, follow_redirects: true)).to eq(record)
expect(described_class.find_by_full_path(record.full_path.upcase, follow_redirects: true)).to eq(record)
end
it 'finds records by their redirected path' do
expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(record)
expect(described_class.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(record)
end
it 'returns nil for unknown paths' do
expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to be_nil
end
end
end
end
end
RSpec.describe Routable do
it_behaves_like '.find_by_full_path' do
let_it_be(:record) { create(:group) }
end
it_behaves_like '.find_by_full_path' do
let_it_be(:record) { create(:project) }
end
end
RSpec.describe Group, 'Routable' do RSpec.describe Group, 'Routable' do
let!(:group) { create(:group, name: 'foo') } let_it_be_with_reload(:group) { create(:group, name: 'foo') }
let_it_be(:nested_group) { create(:group, parent: group) }
describe 'Validations' do describe 'Validations' do
it { is_expected.to validate_presence_of(:route) } it { is_expected.to validate_presence_of(:route) }
...@@ -59,61 +120,20 @@ RSpec.describe Group, 'Routable' do ...@@ -59,61 +120,20 @@ RSpec.describe Group, 'Routable' do
end end
describe '.find_by_full_path' do describe '.find_by_full_path' do
let!(:nested_group) { create(:group, parent: group) } it_behaves_like '.find_by_full_path' do
let_it_be(:record) { group }
context 'without any redirect routes' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
it 'includes route information when loading a record' do
path = group.to_param
control_count = ActiveRecord::QueryRecorder.new { described_class.find_by_full_path(path) }.count
expect { described_class.find_by_full_path(path).route }.not_to exceed_all_query_limit(control_count)
end
end end
context 'with redirect routes' do it_behaves_like '.find_by_full_path' do
let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') } let_it_be(:record) { nested_group }
let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) } end
context 'without follow_redirects option' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
end
context 'with the given path matching the canonical route' do
it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do
it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
end
end
context 'with follow_redirects option set to true' do
context 'with the given path not matching any route' do
it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
end
context 'with the given path matching the canonical route' do it 'does not find projects with a matching path' do
it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) } project = create(:project)
it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) } redirect_route = create(:redirect_route, source: project)
it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
end
context 'with the given path matching a redirect route' do expect(described_class.find_by_full_path(project.full_path)).to be_nil
it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) } expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
end
end
end end
end end
...@@ -131,8 +151,6 @@ RSpec.describe Group, 'Routable' do ...@@ -131,8 +151,6 @@ RSpec.describe Group, 'Routable' do
end end
context 'with valid paths' do context 'with valid paths' do
let!(:nested_group) { create(:group, parent: group) }
it 'returns the projects matching the paths' do it 'returns the projects matching the paths' do
result = described_class.where_full_path_in([group.to_param, nested_group.to_param]) result = described_class.where_full_path_in([group.to_param, nested_group.to_param])
...@@ -148,32 +166,36 @@ RSpec.describe Group, 'Routable' do ...@@ -148,32 +166,36 @@ RSpec.describe Group, 'Routable' do
end end
describe '#full_path' do describe '#full_path' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_path).to eq(group.path) } it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
end end
describe '#full_name' do describe '#full_name' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_name).to eq(group.name) } it { expect(group.full_name).to eq(group.name) }
it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
end end
end end
RSpec.describe Project, 'Routable' do RSpec.describe Project, 'Routable' do
describe '#full_path' do let_it_be(:project) { create(:project) }
let(:project) { build_stubbed(:project) }
it_behaves_like '.find_by_full_path' do
let_it_be(:record) { project }
end
it 'does not find groups with a matching path' do
group = create(:group)
redirect_route = create(:redirect_route, source: group)
expect(described_class.find_by_full_path(group.full_path)).to be_nil
expect(described_class.find_by_full_path(redirect_route.path, follow_redirects: true)).to be_nil
end
describe '#full_path' do
it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" } it { expect(project.full_path).to eq "#{project.namespace.full_path}/#{project.path}" }
end end
describe '#full_name' do describe '#full_name' do
let(:project) { build_stubbed(:project) }
it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" } it { expect(project.full_name).to eq "#{project.namespace.human_name} / #{project.name}" }
end end
end end
...@@ -504,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do ...@@ -504,6 +504,17 @@ RSpec.shared_examples 'wiki controller actions' do
end end
end end
describe '#git_access' do
render_views
it 'renders the git access page' do
get :git_access, params: routing_params
expect(response).to render_template('shared/wikis/git_access')
expect(response.body).to include(wiki.http_url_to_repo)
end
end
def redirect_to_wiki(wiki, page) def redirect_to_wiki(wiki, page)
redirect_to(controller.wiki_page_path(wiki, page)) redirect_to(controller.wiki_page_path(wiki, page))
end end
......
...@@ -16,16 +16,6 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do ...@@ -16,16 +16,6 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
expect(rendered).to have_link('Clone repository') expect(rendered).to have_link('Clone repository')
end end
context 'the wiki is not a project wiki' do
it 'does not include the clone repository link' do
allow(wiki).to receive(:container).and_return(create(:group))
render
expect(rendered).not_to have_link('Clone repository')
end
end
context 'the sidebar failed to load' do context 'the sidebar failed to load' do
before do before do
assign(:sidebar_error, Object.new) assign(:sidebar_error, Object.new)
......
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