2017-04-05 07:44:23 -04:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2017-04-12 05:28:23 -04:00
|
|
|
describe DynamicPathValidator do
|
2017-04-05 09:41:00 -04:00
|
|
|
let(:validator) { described_class.new(attributes: [:path]) }
|
2017-04-10 13:27:19 -04:00
|
|
|
|
|
|
|
# Pass in a full path to remove the format segment:
|
|
|
|
# `/ci/lint(.:format)` -> `/ci/lint`
|
|
|
|
def without_format(path)
|
|
|
|
path.split('(', 2)[0]
|
|
|
|
end
|
|
|
|
|
|
|
|
# Pass in a full path and get the last segment before a wildcard
|
|
|
|
# That's not a parameter
|
|
|
|
# `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
|
2017-04-18 10:27:11 -04:00
|
|
|
# -> 'builds/artifacts'
|
2017-04-28 11:53:28 -04:00
|
|
|
def path_before_wildcard(path)
|
2017-04-18 10:27:11 -04:00
|
|
|
path = path.gsub(STARTING_WITH_NAMESPACE, "")
|
|
|
|
path_segments = path.split('/').reject(&:empty?)
|
2017-04-28 11:53:28 -04:00
|
|
|
wildcard_index = path_segments.index { |segment| parameter?(segment) }
|
2017-04-18 10:27:11 -04:00
|
|
|
|
|
|
|
segments_before_wildcard = path_segments[0..wildcard_index - 1]
|
|
|
|
|
|
|
|
segments_before_wildcard.join('/')
|
2017-04-10 13:27:19 -04:00
|
|
|
end
|
|
|
|
|
2017-04-28 11:53:28 -04:00
|
|
|
def parameter?(segment)
|
|
|
|
segment =~ /[*:]/
|
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
# If the path is reserved. Then no conflicting paths can# be created for any
|
|
|
|
# route using this reserved word.
|
|
|
|
#
|
2017-04-28 11:53:28 -04:00
|
|
|
# Both `builds/artifacts` & `build` are covered by reserving the word
|
|
|
|
# `build`
|
2017-04-18 10:27:11 -04:00
|
|
|
def wildcards_include?(path)
|
|
|
|
described_class::WILDCARD_ROUTES.include?(path) ||
|
2017-04-28 11:53:28 -04:00
|
|
|
described_class::WILDCARD_ROUTES.include?(path.split('/').first)
|
2017-04-10 13:27:19 -04:00
|
|
|
end
|
|
|
|
|
2017-05-02 05:48:54 -04:00
|
|
|
def failure_message(missing_words, constant_name, migration_helper)
|
|
|
|
missing_words = Array(missing_words)
|
|
|
|
<<-MSG
|
|
|
|
Found new routes that could cause conflicts with existing namespaced routes
|
|
|
|
for groups or projects.
|
|
|
|
|
|
|
|
Add <#{missing_words.join(', ')}> to `DynamicPathValidator::#{constant_name}
|
|
|
|
to make sure no projects or namespaces can be created with those paths.
|
|
|
|
|
|
|
|
To rename any existing records with those paths you can use the
|
|
|
|
`Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
|
|
|
|
migration helper.
|
|
|
|
|
|
|
|
Make sure to make a note of the renamed records in the release blog post.
|
|
|
|
|
|
|
|
MSG
|
|
|
|
end
|
|
|
|
|
2017-04-10 13:27:19 -04:00
|
|
|
let(:all_routes) do
|
|
|
|
Rails.application.routes.routes.routes.
|
|
|
|
map { |r| r.path.spec.to_s }
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
|
|
|
|
|
|
|
|
# Routes not starting with `/:` or `/*`
|
|
|
|
# all routes not starting with a param
|
|
|
|
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
let(:top_level_words) do
|
|
|
|
routes_not_starting_in_wildcard.map do |route|
|
|
|
|
route.split('/')[1]
|
|
|
|
end.compact.uniq
|
|
|
|
end
|
|
|
|
|
2017-04-10 13:27:19 -04:00
|
|
|
# All routes that start with a namespaced path, that have 1 or more
|
|
|
|
# path-segments before having another wildcard parameter.
|
|
|
|
# - Starting with paths:
|
|
|
|
# - `/*namespace_id/:project_id/`
|
|
|
|
# - `/*namespace_id/:id/`
|
2017-04-18 10:27:11 -04:00
|
|
|
# - Followed by one or more path-parts not starting with `:` or `*`
|
2017-04-10 13:27:19 -04:00
|
|
|
# - Followed by a path-part that includes a wildcard parameter `*`
|
2017-04-18 10:27:11 -04:00
|
|
|
# At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
|
2017-04-24 06:22:51 -04:00
|
|
|
STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
|
|
|
|
NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
|
|
|
|
ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
|
|
|
|
WILDCARD_SEGMENT = %r{\*}
|
2017-04-10 13:27:19 -04:00
|
|
|
let(:namespaced_wildcard_routes) do
|
|
|
|
routes_without_format.select do |p|
|
2017-04-24 06:22:51 -04:00
|
|
|
p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
|
2017-04-10 13:27:19 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
# This will return all paths that are used in a namespaced route
|
|
|
|
# before another wildcard path:
|
|
|
|
#
|
|
|
|
# /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
|
|
|
|
# /*namespace_id/:project_id/info/lfs/objects/*oid
|
|
|
|
# /*namespace_id/:project_id/commits/*id
|
|
|
|
# /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
|
|
|
|
# -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
|
|
|
|
let(:all_wildcard_paths) do
|
|
|
|
namespaced_wildcard_routes.map do |route|
|
2017-04-28 11:53:28 -04:00
|
|
|
path_before_wildcard(route)
|
|
|
|
end.uniq
|
|
|
|
end
|
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
|
|
|
|
let(:group_routes) do
|
|
|
|
routes_without_format.select do |path|
|
|
|
|
path =~ STARTING_WITH_GROUP
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
let(:paths_after_group_id) do
|
|
|
|
group_routes.map do |route|
|
|
|
|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
|
2017-04-18 10:27:11 -04:00
|
|
|
end.uniq
|
|
|
|
end
|
|
|
|
|
2017-04-10 13:27:19 -04:00
|
|
|
describe 'TOP_LEVEL_ROUTES' do
|
2017-04-05 07:44:23 -04:00
|
|
|
it 'includes all the top level namespaces' do
|
2017-05-02 05:48:54 -04:00
|
|
|
failure_block = lambda do
|
|
|
|
missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
|
|
|
|
failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(described_class::TOP_LEVEL_ROUTES)
|
|
|
|
.to include(*top_level_words), failure_block
|
2017-04-05 07:44:23 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
describe 'GROUP_ROUTES' do
|
|
|
|
it "don't contain a second wildcard" do
|
2017-05-02 05:48:54 -04:00
|
|
|
failure_block = lambda do
|
|
|
|
missing_words = paths_after_group_id - described_class::GROUP_ROUTES
|
|
|
|
failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
|
|
|
|
end
|
|
|
|
|
|
|
|
expect(described_class::GROUP_ROUTES)
|
|
|
|
.to include(*paths_after_group_id), failure_block
|
2017-04-28 12:09:01 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-04-05 07:44:23 -04:00
|
|
|
describe 'WILDCARD_ROUTES' do
|
|
|
|
it 'includes all paths that can be used after a namespace/project path' do
|
2017-04-18 10:27:11 -04:00
|
|
|
aggregate_failures do
|
|
|
|
all_wildcard_paths.each do |path|
|
2017-05-02 05:48:54 -04:00
|
|
|
expect(wildcards_include?(path))
|
|
|
|
.to be(true), failure_message(path, 'WILDCARD_ROUTES', 'rename_wildcard_paths')
|
2017-04-18 10:27:11 -04:00
|
|
|
end
|
|
|
|
end
|
2017-04-05 07:44:23 -04:00
|
|
|
end
|
|
|
|
end
|
2017-04-05 09:41:00 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
describe '.without_reserved_wildcard_paths_regex' do
|
|
|
|
subject { described_class.without_reserved_wildcard_paths_regex }
|
2017-04-11 09:51:33 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
it 'rejects paths starting with a reserved top level' do
|
|
|
|
expect(subject).not_to match('dashboard/hello/world')
|
|
|
|
expect(subject).not_to match('dashboard')
|
2017-04-11 09:51:33 -04:00
|
|
|
end
|
|
|
|
|
2017-05-02 03:13:10 -04:00
|
|
|
it 'matches valid paths with a toplevel word in a different place' do
|
|
|
|
expect(subject).to match('parent/dashboard/project-path')
|
|
|
|
end
|
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
it 'rejects paths containing a wildcard reserved word' do
|
|
|
|
expect(subject).not_to match('hello/edit')
|
|
|
|
expect(subject).not_to match('hello/edit/in-the-middle')
|
|
|
|
expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
|
2017-04-18 10:27:11 -04:00
|
|
|
end
|
2017-04-11 09:51:33 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
it 'matches valid paths' do
|
|
|
|
expect(subject).to match('parent/child/project-path')
|
2017-04-11 09:51:33 -04:00
|
|
|
end
|
2017-04-28 12:09:01 -04:00
|
|
|
end
|
2017-04-11 09:51:33 -04:00
|
|
|
|
2017-05-02 04:32:31 -04:00
|
|
|
describe '.regex_excluding_child_paths' do
|
|
|
|
let(:subject) { described_class.without_reserved_child_paths_regex }
|
2017-04-11 09:51:33 -04:00
|
|
|
|
2017-05-02 04:32:31 -04:00
|
|
|
it 'rejects paths containing a child reserved word' do
|
2017-04-28 12:09:01 -04:00
|
|
|
expect(subject).not_to match('hello/group_members')
|
|
|
|
expect(subject).not_to match('hello/activity/in-the-middle')
|
|
|
|
expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
|
2017-04-11 09:51:33 -04:00
|
|
|
end
|
2017-05-02 04:32:31 -04:00
|
|
|
|
|
|
|
it 'allows a child path on the top level' do
|
|
|
|
expect(subject).to match('activity/foo')
|
|
|
|
expect(subject).to match('avatar')
|
|
|
|
end
|
2017-04-11 09:51:33 -04:00
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
describe ".valid?" do
|
|
|
|
it 'is not case sensitive' do
|
2017-04-28 12:09:01 -04:00
|
|
|
expect(described_class.valid?("Users")).to be_falsey
|
2017-04-05 09:41:00 -04:00
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
it "isn't valid when the top level is reserved" do
|
|
|
|
test_path = 'u/should-be-a/reserved-word'
|
2017-04-05 09:41:00 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
expect(described_class.valid?(test_path)).to be_falsey
|
2017-04-05 09:41:00 -04:00
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
it "isn't valid if any of the path segments is reserved" do
|
|
|
|
test_path = 'the-wildcard/wikis/is-not-allowed'
|
2017-04-05 09:41:00 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
expect(described_class.valid?(test_path)).to be_falsey
|
2017-04-05 09:41:00 -04:00
|
|
|
end
|
|
|
|
|
2017-04-18 10:27:11 -04:00
|
|
|
it "is valid if the path doesn't contain reserved words" do
|
|
|
|
test_path = 'there-are/no-wildcards/in-this-path'
|
2017-04-05 09:41:00 -04:00
|
|
|
|
2017-04-28 12:09:01 -04:00
|
|
|
expect(described_class.valid?(test_path)).to be_truthy
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'allows allows a child path on the last spot' do
|
|
|
|
test_path = 'there/can-be-a/project-called/labels'
|
|
|
|
|
|
|
|
expect(described_class.valid?(test_path)).to be_truthy
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'rejects a child path somewhere else' do
|
|
|
|
test_path = 'there/can-be-no/labels/group'
|
|
|
|
|
|
|
|
expect(described_class.valid?(test_path)).to be_falsey
|
2017-04-05 09:41:00 -04:00
|
|
|
end
|
2017-05-02 03:13:10 -04:00
|
|
|
|
|
|
|
it 'rejects paths that are in an incorrect format' do
|
|
|
|
test_path = 'incorrect/format.git'
|
|
|
|
|
|
|
|
expect(described_class.valid?(test_path)).to be_falsey
|
|
|
|
end
|
2017-04-05 09:41:00 -04:00
|
|
|
end
|
2017-05-02 04:32:31 -04:00
|
|
|
|
|
|
|
describe '#path_reserved_for_record?' do
|
|
|
|
it 'reserves a sub-group named activity' do
|
|
|
|
group = build(:group, :nested, path: 'activity')
|
|
|
|
|
|
|
|
expect(validator.path_reserved_for_record?(group, 'activity')).to be_truthy
|
|
|
|
end
|
|
|
|
|
|
|
|
it "doesn't reserve a project called activity" do
|
|
|
|
project = build(:project, path: 'activity')
|
|
|
|
|
|
|
|
expect(validator.path_reserved_for_record?(project, 'activity')).to be_falsey
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe '#validates_each' do
|
|
|
|
it 'adds a message when the path is not in the correct format' do
|
|
|
|
group = build(:group)
|
|
|
|
|
|
|
|
validator.validate_each(group, :path, "Path with spaces, and comma's!")
|
|
|
|
|
|
|
|
expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message)
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'adds a message when the path is not in the correct format' do
|
|
|
|
group = build(:group, path: 'users')
|
|
|
|
|
|
|
|
validator.validate_each(group, :path, 'users')
|
|
|
|
|
|
|
|
expect(group.errors[:path]).to include('users is a reserved name')
|
|
|
|
end
|
|
|
|
end
|
2017-04-05 07:44:23 -04:00
|
|
|
end
|