puppet_forge 1.0.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/CHANGELOG.md +23 -0
  2. data/MAINTAINERS +13 -0
  3. data/README.md +48 -6
  4. data/lib/puppet_forge.rb +4 -0
  5. data/lib/puppet_forge/connection.rb +81 -0
  6. data/lib/puppet_forge/connection/connection_failure.rb +26 -0
  7. data/lib/puppet_forge/error.rb +34 -0
  8. data/lib/{her → puppet_forge}/lazy_accessors.rb +20 -27
  9. data/lib/{her → puppet_forge}/lazy_relations.rb +28 -9
  10. data/lib/puppet_forge/middleware/symbolify_json.rb +72 -0
  11. data/lib/puppet_forge/tar.rb +10 -0
  12. data/lib/puppet_forge/tar/mini.rb +81 -0
  13. data/lib/puppet_forge/unpacker.rb +68 -0
  14. data/lib/puppet_forge/v3.rb +11 -0
  15. data/lib/puppet_forge/v3/base.rb +106 -73
  16. data/lib/puppet_forge/v3/base/paginated_collection.rb +23 -14
  17. data/lib/puppet_forge/v3/metadata.rb +197 -0
  18. data/lib/puppet_forge/v3/module.rb +2 -1
  19. data/lib/puppet_forge/v3/release.rb +33 -8
  20. data/lib/puppet_forge/v3/user.rb +2 -0
  21. data/lib/puppet_forge/version.rb +1 -1
  22. data/puppet_forge.gemspec +6 -3
  23. data/spec/fixtures/v3/modules/puppetlabs-apache.json +21 -1
  24. data/spec/fixtures/v3/releases/puppetlabs-apache-0.0.1.json +4 -1
  25. data/spec/integration/forge/v3/module_spec.rb +79 -0
  26. data/spec/integration/forge/v3/release_spec.rb +75 -0
  27. data/spec/integration/forge/v3/user_spec.rb +70 -0
  28. data/spec/spec_helper.rb +15 -8
  29. data/spec/unit/forge/connection/connection_failure_spec.rb +30 -0
  30. data/spec/unit/forge/connection_spec.rb +53 -0
  31. data/spec/unit/{her → forge}/lazy_accessors_spec.rb +20 -13
  32. data/spec/unit/{her → forge}/lazy_relations_spec.rb +60 -46
  33. data/spec/unit/forge/middleware/symbolify_json_spec.rb +63 -0
  34. data/spec/unit/forge/tar/mini_spec.rb +85 -0
  35. data/spec/unit/forge/tar_spec.rb +9 -0
  36. data/spec/unit/forge/unpacker_spec.rb +58 -0
  37. data/spec/unit/forge/v3/base/paginated_collection_spec.rb +68 -46
  38. data/spec/unit/forge/v3/base_spec.rb +1 -1
  39. data/spec/unit/forge/v3/metadata_spec.rb +300 -0
  40. data/spec/unit/forge/v3/module_spec.rb +14 -36
  41. data/spec/unit/forge/v3/release_spec.rb +9 -30
  42. data/spec/unit/forge/v3/user_spec.rb +7 -7
  43. metadata +127 -41
  44. checksums.yaml +0 -7
  45. data/lib/puppet_forge/middleware/json_for_her.rb +0 -37
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetForge::Middleware::SymbolifyJson do
4
+ let(:basic_array) { [1, "two", 3] }
5
+ let(:basic_hash) { { "id" => 1, "data" => "x" } }
6
+ let(:symbolified_hash) { { :id => 1, :data => "x" } }
7
+ let(:internal_hash) { { :id => 2, :data => basic_hash } }
8
+
9
+ let(:hash_with_array) { { "id" => 3, "data" => basic_array } }
10
+ let(:array_with_hash) { [1, "two", basic_hash] }
11
+
12
+ let(:complex_array) { [array_with_hash, hash_with_array] }
13
+ let(:complex_hash) { { "id" => 4, "data" => [complex_array, basic_array], "more_data" => hash_with_array } }
14
+ let(:complex_request) { { "id" => 5, "data" => complex_hash } }
15
+
16
+ let(:middleware) { described_class.new() }
17
+
18
+ context "#process_array" do
19
+ it "doesn't change an array with no array or hash inside" do
20
+ processed_array = middleware.process_array(basic_array)
21
+ expect(processed_array).to eql( [1, "two", 3] )
22
+ end
23
+
24
+ it "changes all keys of a hash inside the array" do
25
+ processed_array = middleware.process_array(array_with_hash)
26
+ expect(processed_array).to eql( [ 1, "two", { :id => 1, :data => "x" } ] )
27
+ end
28
+ end
29
+
30
+ context "#process_hash" do
31
+ it "changes all keys that respond to :to_sym into Symbols and doesn't change values." do
32
+ processed_hash = middleware.process_hash(basic_hash)
33
+ expect(processed_hash).to eql( { :id => 1, :data => "x" } )
34
+ end
35
+
36
+ it "doesn't change keys that don't respond to :to_sym" do
37
+ processed_hash = middleware.process_hash(basic_hash.merge({ 1 => 2 }))
38
+ expect(processed_hash).to eql( { :id => 1, :data => "x", 1 => 2 } )
39
+ end
40
+
41
+ it "can process a hash that is already symbolified" do
42
+ processed_hash = middleware.process_hash(symbolified_hash)
43
+ expect(processed_hash).to eql( { :id => 1, :data => "x" })
44
+ end
45
+
46
+ it "can process a hash with a hash inside of it" do
47
+ processed_hash = middleware.process_hash(internal_hash)
48
+ expect(processed_hash).to eql( {:id => 2, :data => { :id => 1, :data => "x" } })
49
+ end
50
+
51
+ it "can process a hash with an array inside of it" do
52
+ processed_hash = middleware.process_hash(hash_with_array)
53
+ expect(processed_hash).to eql( { :id => 3, :data => [1, "two", 3] } )
54
+ end
55
+
56
+ it "can handle extensively nested arrays and hashes" do
57
+ processed_hash = middleware.process_hash(complex_request)
58
+ expect(processed_hash).to eql( { :id => 5, :data => { :id => 4 , :data=>[ [ [1, "two", { :id => 1, :data => "x" } ], { :id=>3, :data => [1, "two", 3] } ], [1, "two", 3] ], :more_data => { :id => 3, :data => [1, "two", 3] } } } )
59
+ end
60
+ end
61
+
62
+ end
63
+
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetForge::Tar::Mini do
4
+ let(:entry_class) do
5
+ Class.new do
6
+ attr_accessor :typeflag, :name
7
+ def initialize(name, typeflag)
8
+ @name = name
9
+ @typeflag = typeflag
10
+ end
11
+ end
12
+ end
13
+ let(:sourcefile) { '/the/module.tar.gz' }
14
+ let(:destdir) { File.expand_path '/the/dest/dir' }
15
+ let(:sourcedir) { '/the/src/dir' }
16
+ let(:destfile) { '/the/dest/file.tar.gz' }
17
+ let(:minitar) { described_class.new }
18
+ let(:tarfile_contents) { [entry_class.new('file', '0'), \
19
+ entry_class.new('symlink', '2'), \
20
+ entry_class.new('invalid', 'F')] }
21
+
22
+ it "unpacks a tar file" do
23
+ unpacks_the_entry(:file_start, 'thefile')
24
+
25
+ minitar.unpack(sourcefile, destdir)
26
+ end
27
+
28
+ it "does not allow an absolute path" do
29
+ unpacks_the_entry(:file_start, '/thefile')
30
+
31
+ expect {
32
+ minitar.unpack(sourcefile, destdir)
33
+ }.to raise_error(PuppetForge::InvalidPathInPackageError,
34
+ "Attempt to install file into \"/thefile\" under \"#{destdir}\"")
35
+ end
36
+
37
+ it "does not allow a file to be written outside the destination directory" do
38
+ unpacks_the_entry(:file_start, '../../thefile')
39
+
40
+ expect {
41
+ minitar.unpack(sourcefile, destdir)
42
+ }.to raise_error(PuppetForge::InvalidPathInPackageError,
43
+ "Attempt to install file into \"#{File.expand_path('/the/thefile')}\" under \"#{destdir}\"")
44
+ end
45
+
46
+ it "does not allow a directory to be written outside the destination directory" do
47
+ unpacks_the_entry(:dir, '../../thedir')
48
+
49
+ expect {
50
+ minitar.unpack(sourcefile, destdir)
51
+ }.to raise_error(PuppetForge::InvalidPathInPackageError,
52
+ "Attempt to install file into \"#{File.expand_path('/the/thedir')}\" under \"#{destdir}\"")
53
+ end
54
+
55
+ it "packs a tar file" do
56
+ writer = double('GzipWriter')
57
+
58
+ expect(Zlib::GzipWriter).to receive(:open).with(destfile).and_yield(writer)
59
+ expect(Archive::Tar::Minitar).to receive(:pack).with(sourcedir, writer)
60
+
61
+ minitar.pack(sourcedir, destfile)
62
+ end
63
+
64
+ it "returns filenames in a tar separated into correct categories" do
65
+ reader = double('GzipReader')
66
+
67
+ expect(Zlib::GzipReader).to receive(:open).with(sourcefile).and_yield(reader)
68
+ expect(Archive::Tar::Minitar).to receive(:open).with(reader).and_return(tarfile_contents)
69
+ expect(Archive::Tar::Minitar).to receive(:unpack).with(reader, destdir, ['file']).and_yield(:file_start, 'thefile', nil)
70
+
71
+ file_lists = minitar.unpack(sourcefile, destdir)
72
+
73
+ expect(file_lists[:valid]).to eq(['file'])
74
+ expect(file_lists[:invalid]).to eq(['invalid'])
75
+ expect(file_lists[:symlinks]).to eq(['symlink'])
76
+ end
77
+
78
+ def unpacks_the_entry(type, name)
79
+ reader = double('GzipReader')
80
+
81
+ expect(Zlib::GzipReader).to receive(:open).with(sourcefile).and_yield(reader)
82
+ expect(minitar).to receive(:validate_files).with(reader).and_return({:valid => [name]})
83
+ expect(Archive::Tar::Minitar).to receive(:unpack).with(reader, destdir, [name]).and_yield(type, name, nil)
84
+ end
85
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetForge::Tar do
4
+
5
+ it "returns an instance of minitar" do
6
+ expect(described_class.instance).to be_a_kind_of PuppetForge::Tar::Mini
7
+ end
8
+
9
+ end
@@ -0,0 +1,58 @@
1
+ require 'tmpdir'
2
+ require 'spec_helper'
3
+
4
+ describe PuppetForge::Unpacker do
5
+
6
+ let(:source) { Dir.mktmpdir("source") }
7
+ let(:target) { Dir.mktmpdir("unpacker") }
8
+ let(:module_name) { 'myusername-mytarball' }
9
+ let(:filename) { Dir.mktmpdir("module") + "/module.tar.gz" }
10
+ let(:working_dir) { Dir.mktmpdir("working_dir") }
11
+ let(:trash_dir) { Dir.mktmpdir("trash_dir") }
12
+
13
+ it "attempts to untar file to temporary location" do
14
+
15
+ minitar = double('PuppetForge::Tar::Mini')
16
+
17
+ expect(minitar).to receive(:unpack).with(filename, anything()) do |src, dest|
18
+ FileUtils.mkdir(File.join(dest, 'extractedmodule'))
19
+ File.open(File.join(dest, 'extractedmodule', 'metadata.json'), 'w+') do |file|
20
+ file.puts JSON.generate('name' => module_name, 'version' => '1.0.0')
21
+ end
22
+ true
23
+ end
24
+
25
+ expect(PuppetForge::Tar).to receive(:instance).and_return(minitar)
26
+ PuppetForge::Unpacker.unpack(filename, target, trash_dir)
27
+ expect(File).to be_directory(target)
28
+ end
29
+
30
+ it "returns the appropriate categories of the contents of the tar file from the tar implementation" do
31
+
32
+ minitar = double('PuppetForge::Tar::Mini')
33
+
34
+ expect(minitar).to receive(:unpack).with(filename, anything()) do |src, dest|
35
+ FileUtils.mkdir(File.join(dest, 'extractedmodule'))
36
+ File.open(File.join(dest, 'extractedmodule', 'metadata.json'), 'w+') do |file|
37
+ file.puts JSON.generate('name' => module_name, 'version' => '1.0.0')
38
+ end
39
+ { :valid => [File.join('extractedmodule', 'metadata.json')], :invalid => [], :symlinks => [] }
40
+ end
41
+
42
+ expect(PuppetForge::Tar).to receive(:instance).and_return(minitar)
43
+ file_lists = PuppetForge::Unpacker.unpack(filename, target, trash_dir)
44
+ expect(file_lists).to eq({:valid=>["extractedmodule/metadata.json"], :invalid=>[], :symlinks=>[]})
45
+ expect(File).to be_directory(target)
46
+ end
47
+
48
+ it "attempts to set the ownership of a target dir to a source dir's owner" do
49
+
50
+ source_path = Pathname.new(source)
51
+ target_path = Pathname.new(target)
52
+
53
+ expect(FileUtils).to receive(:chown_R).with(source_path.stat.uid, source_path.stat.gid, target_path)
54
+
55
+ PuppetForge::Unpacker.harmonize_ownership(source_path, target_path)
56
+ end
57
+
58
+ end
@@ -2,53 +2,65 @@ require 'spec_helper'
2
2
 
3
3
  describe PuppetForge::V3::Base::PaginatedCollection do
4
4
  let(:klass) do
5
- Class.new do
6
- def self.get_collection(url)
7
- data = {
8
- '/v3/collection' => [ :A, :B, :C ],
9
- '/v3/collection?page=2' => [ :D, :E, :F ],
10
- '/v3/collection?page=3' => [ :G, :H ],
11
- }
12
-
13
- meta = {
14
- '/v3/collection' => {
15
- :limit => 3,
16
- :offset => 0,
17
- :first => '/v3/collection',
18
- :previous => nil,
19
- :current => '/v3/collection',
20
- :next => '/v3/collection?page=2',
21
- :total => 8,
22
- },
23
- '/v3/collection?page=2' => {
24
- :limit => 3,
25
- :offset => 0,
26
- :first => '/v3/collection',
27
- :previous => '/v3/collection',
28
- :current => '/v3/collection?page=2',
29
- :next => '/v3/collection?page=3',
30
- :total => 8,
31
- },
32
- '/v3/collection?page=3' => {
33
- :limit => 3,
34
- :offset => 0,
35
- :first => '/v3/collection',
36
- :previous => '/v3/collection?page=2',
37
- :current => '/v3/collection?page=3',
38
- :next => nil,
39
- :total => 8,
40
- },
41
- }
42
-
43
- PuppetForge::V3::Base::PaginatedCollection.new(self, data[url], meta[url], {})
44
- end
5
+ allow(PuppetForge::V3::Base).to receive(:get_collection) do |url|
6
+ data = {
7
+ '/v3/collection' => [ { :data => :A }, { :data => :B }, { :data => :C } ],
8
+ '/v3/collection?page=2' => [ { :data => :D }, { :data => :E }, { :data => :F } ],
9
+ '/v3/collection?page=3' => [ { :data => :G }, { :data => :H } ],
10
+ }
11
+
12
+ meta = {
13
+ '/v3/collection' => {
14
+ :limit => 3,
15
+ :offset => 0,
16
+ :first => '/v3/collection',
17
+ :previous => nil,
18
+ :current => '/v3/collection',
19
+ :next => '/v3/collection?page=2',
20
+ :total => 8,
21
+ },
22
+ '/v3/collection?page=2' => {
23
+ :limit => 3,
24
+ :offset => 0,
25
+ :first => '/v3/collection',
26
+ :previous => '/v3/collection',
27
+ :current => '/v3/collection?page=2',
28
+ :next => '/v3/collection?page=3',
29
+ :total => 8,
30
+ },
31
+ '/v3/collection?page=3' => {
32
+ :limit => 3,
33
+ :offset => 0,
34
+ :first => '/v3/collection',
35
+ :previous => '/v3/collection?page=2',
36
+ :current => '/v3/collection?page=3',
37
+ :next => nil,
38
+ :total => 8,
39
+ },
40
+ }
41
+
42
+ PuppetForge::V3::Base::PaginatedCollection.new(PuppetForge::V3::Base, data[url], meta[url], {})
45
43
  end
44
+
45
+ PuppetForge::V3::Base
46
46
  end
47
47
 
48
48
  subject { klass.get_collection('/v3/collection') }
49
49
 
50
+ def collect_data(paginated)
51
+ paginated.to_a.collect do |x|
52
+ x.data
53
+ end
54
+ end
55
+
56
+ it '#all returns self for backwards compatibility.' do
57
+ paginated = subject.all
58
+
59
+ expect(paginated).to eq(subject)
60
+ end
61
+
50
62
  it 'maps to a single page of the collection' do
51
- expect(subject.to_a).to eql([ :A, :B, :C ])
63
+ expect(collect_data(subject)).to eql([ :A, :B, :C ])
52
64
  end
53
65
 
54
66
  it 'knows the size of the entire collection' do
@@ -61,12 +73,12 @@ describe PuppetForge::V3::Base::PaginatedCollection do
61
73
 
62
74
  it 'enables page navigation' do
63
75
  expect(subject.next).to_not be_empty
64
- expect(subject.next.to_a).to_not eql(subject.to_a)
65
- expect(subject.next.previous.to_a).to eql(subject.to_a)
76
+ expect(collect_data(subject.next)).to_not eql(collect_data(subject))
77
+ expect(collect_data(subject.next.previous)).to eql(collect_data(subject))
66
78
  end
67
79
 
68
80
  it 'exposes the pagination metadata' do
69
- expect(subject.metadata[:limit]).to be subject.size
81
+ expect(subject.limit).to be subject.size
70
82
  end
71
83
 
72
84
  it 'exposes previous_url and next_url' do
@@ -77,12 +89,22 @@ describe PuppetForge::V3::Base::PaginatedCollection do
77
89
  describe '#unpaginated' do
78
90
  it 'provides an iterator over the entire collection' do
79
91
  expected = [ :A, :B, :C, :D, :E, :F, :G, :H ]
80
- expect(subject.unpaginated.to_a).to eql(expected)
92
+ actual = subject.unpaginated.to_a.collect do |x|
93
+ expect(x).to be_a(klass)
94
+ x.data
95
+ end
96
+
97
+ expect(actual).to eql(expected)
81
98
  end
82
99
 
83
100
  it "provides a full iterator regardless of which page it's started on" do
84
101
  expected = [ :A, :B, :C, :D, :E, :F, :G, :H ]
85
- expect(subject.next.next.unpaginated.to_a).to eql(expected)
102
+
103
+ actual = subject.next.next.unpaginated.to_a.collect do |x|
104
+ expect(x).to be_a(klass)
105
+ x.data
106
+ end
107
+ expect(actual).to eql(expected)
86
108
  end
87
109
  end
88
110
  end
@@ -13,7 +13,7 @@ describe PuppetForge::V3::Base do
13
13
 
14
14
  collection = PuppetForge::V3::Base.new_collection(response_data)
15
15
 
16
- expect(collection.limit).to eq(10)
16
+ expect(collection.limit).to eq(20)
17
17
  expect(collection.offset).to eq(0)
18
18
  expect(collection.total).to eq(0)
19
19
  end
@@ -0,0 +1,300 @@
1
+ require 'spec_helper'
2
+
3
+ describe PuppetForge::Metadata do
4
+ let(:data) { {} }
5
+ let(:metadata) { PuppetForge::Metadata.new }
6
+
7
+ describe 'property lookups' do
8
+ subject { metadata }
9
+
10
+ %w[ name version author summary license source project_page issues_url
11
+ dependencies dashed_name release_name description ].each do |prop|
12
+ describe "##{prop}" do
13
+ it "responds to the property" do
14
+ subject.send(prop)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ describe "#update" do
21
+ subject { metadata.update(data) }
22
+
23
+ context "with a valid name" do
24
+ let(:data) { { 'name' => 'billgates-mymodule' } }
25
+
26
+ it "extracts the author name from the name field" do
27
+ expect(subject.to_hash['author']).to eq('billgates')
28
+ end
29
+
30
+ it "extracts a module name from the name field" do
31
+ expect(subject.module_name).to eq('mymodule')
32
+ end
33
+
34
+ context "and existing author" do
35
+ before { metadata.update('author' => 'foo') }
36
+
37
+ it "avoids overwriting the existing author" do
38
+ expect(subject.to_hash['author']).to eq('foo')
39
+ end
40
+ end
41
+ end
42
+
43
+ context "with a valid name and author" do
44
+ let(:data) { { 'name' => 'billgates-mymodule', 'author' => 'foo' } }
45
+
46
+ it "use the author name from the author field" do
47
+ expect(subject.to_hash['author']).to eq('foo')
48
+ end
49
+
50
+ context "and preexisting author" do
51
+ before { metadata.update('author' => 'bar') }
52
+
53
+ it "avoids overwriting the existing author" do
54
+ expect(subject.to_hash['author']).to eq('foo')
55
+ end
56
+ end
57
+ end
58
+
59
+ context "with an invalid name" do
60
+ context "(short module name)" do
61
+ let(:data) { { 'name' => 'mymodule' } }
62
+
63
+ it "raises an exception" do
64
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the field must be a namespaced module name")
65
+ end
66
+ end
67
+
68
+ context "(missing namespace)" do
69
+ let(:data) { { 'name' => '/mymodule' } }
70
+
71
+ it "raises an exception" do
72
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the field must be a namespaced module name")
73
+ end
74
+ end
75
+
76
+ context "(missing module name)" do
77
+ let(:data) { { 'name' => 'namespace/' } }
78
+
79
+ it "raises an exception" do
80
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the field must be a namespaced module name")
81
+ end
82
+ end
83
+
84
+ context "(invalid namespace)" do
85
+ let(:data) { { 'name' => "dolla'bill$-mymodule" } }
86
+
87
+ it "raises an exception" do
88
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the namespace contains non-alphanumeric characters")
89
+ end
90
+ end
91
+
92
+ context "(non-alphanumeric module name)" do
93
+ let(:data) { { 'name' => "dollabils-fivedolla'" } }
94
+
95
+ it "raises an exception" do
96
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the module name contains non-alphanumeric (or underscore) characters")
97
+ end
98
+ end
99
+
100
+ context "(module name starts with a number)" do
101
+ let(:data) { { 'name' => "dollabills-5dollars" } }
102
+
103
+ it "raises an exception" do
104
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'name' field in metadata.json: the module name must begin with a letter")
105
+ end
106
+ end
107
+ end
108
+
109
+
110
+ context "with an invalid version" do
111
+ let(:data) { { 'version' => '3.0' } }
112
+
113
+ it "raises an exception" do
114
+ expect { subject }.to raise_error(ArgumentError, "Invalid 'version' field in metadata.json: version string cannot be parsed as a valid Semantic Version")
115
+ end
116
+ end
117
+
118
+ context "with a valid source" do
119
+ context "which is a GitHub URL" do
120
+ context "with a scheme" do
121
+ before { metadata.update('source' => 'https://github.com/billgates/amazingness') }
122
+
123
+ it "predicts a default project_page" do
124
+ expect(subject.to_hash['project_page']).to eq('https://github.com/billgates/amazingness')
125
+ end
126
+
127
+ it "predicts a default issues_url" do
128
+ expect(subject.to_hash['issues_url']).to eq('https://github.com/billgates/amazingness/issues')
129
+ end
130
+ end
131
+
132
+ context "without a scheme" do
133
+ before { metadata.update('source' => 'github.com/billgates/amazingness') }
134
+
135
+ it "predicts a default project_page" do
136
+ expect(subject.to_hash['project_page']).to eq('https://github.com/billgates/amazingness')
137
+ end
138
+
139
+ it "predicts a default issues_url" do
140
+ expect(subject.to_hash['issues_url']).to eq('https://github.com/billgates/amazingness/issues')
141
+ end
142
+ end
143
+ end
144
+
145
+ context "which is not a GitHub URL" do
146
+ before { metadata.update('source' => 'https://notgithub.com/billgates/amazingness') }
147
+
148
+ it "does not predict a default project_page" do
149
+ expect(subject.to_hash['project_page']).to be nil
150
+ end
151
+
152
+ it "does not predict a default issues_url" do
153
+ expect(subject.to_hash['issues_url']).to be nil
154
+ end
155
+ end
156
+
157
+ context "which is not a URL" do
158
+ before { metadata.update('source' => 'my brain') }
159
+
160
+ it "does not predict a default project_page" do
161
+ expect(subject.to_hash['project_page']).to be nil
162
+ end
163
+
164
+ it "does not predict a default issues_url" do
165
+ expect(subject.to_hash['issues_url']).to be nil
166
+ end
167
+ end
168
+
169
+ end
170
+
171
+ context "with a valid dependency", :pending => "dependency resolution is not yet in scope" do
172
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabs-goodmodule'}] }}
173
+
174
+ it "adds the dependency" do
175
+ expect(subject.dependencies.size).to eq(1)
176
+ end
177
+ end
178
+
179
+ context "with a invalid dependency name" do
180
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabsbadmodule'}] }}
181
+
182
+ it "raises an exception" do
183
+ expect { subject }.to raise_error(ArgumentError)
184
+ end
185
+ end
186
+
187
+ context "with a valid dependency version range", :pending => "dependency resolution is not yet in scope" do
188
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabs-badmodule', 'version_requirement' => '>= 2.0.0'}] }}
189
+
190
+ it "adds the dependency" do
191
+ expect(subject.dependencies.size).to eq(1)
192
+ end
193
+ end
194
+
195
+ context "with a invalid version range" do
196
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabsbadmodule', 'version_requirement' => '>= banana'}] }}
197
+
198
+ it "raises an exception" do
199
+ expect { subject }.to raise_error(ArgumentError)
200
+ end
201
+ end
202
+
203
+ context "with duplicate dependencies", :pending => "dependency resolution is not yet in scope" do
204
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabs-dupmodule', 'version_requirement' => '1.0.0'},
205
+ {'name' => 'puppetlabs-dupmodule', 'version_requirement' => '0.0.1'}] }
206
+ }
207
+
208
+ it "raises an exception" do
209
+ expect { subject }.to raise_error(ArgumentError)
210
+ end
211
+ end
212
+
213
+ context "adding a duplicate dependency", :pending => "dependency resolution is not yet in scope" do
214
+ let(:data) { {'dependencies' => [{'name' => 'puppetlabs-origmodule', 'version_requirement' => '1.0.0'}] }}
215
+
216
+ it "with a different version raises an exception" do
217
+ metadata.add_dependency('puppetlabs-origmodule', '>= 0.0.1')
218
+ expect { subject }.to raise_error(ArgumentError)
219
+ end
220
+
221
+ it "with the same version does not add another dependency" do
222
+ metadata.add_dependency('puppetlabs-origmodule', '1.0.0')
223
+ expect(subject.dependencies.size).to eq(1)
224
+ end
225
+ end
226
+ end
227
+
228
+ describe '#dashed_name' do
229
+ it 'returns nil in the absence of a module name' do
230
+ expect(metadata.update('version' => '1.0.0').release_name).to be_nil
231
+ end
232
+
233
+ it 'returns a hyphenated string containing namespace and module name' do
234
+ data = metadata.update('name' => 'foo-bar')
235
+ expect(data.dashed_name).to eq('foo-bar')
236
+ end
237
+
238
+ it 'properly handles slash-separated names' do
239
+ data = metadata.update('name' => 'foo/bar')
240
+ expect(data.dashed_name).to eq('foo-bar')
241
+ end
242
+
243
+ it 'is unaffected by author name' do
244
+ data = metadata.update('name' => 'foo/bar', 'author' => 'me')
245
+ expect(data.dashed_name).to eq('foo-bar')
246
+ end
247
+ end
248
+
249
+ describe '#release_name' do
250
+ it 'returns nil in the absence of a module name' do
251
+ expect(metadata.update('version' => '1.0.0').release_name).to be_nil
252
+ end
253
+
254
+ it 'returns nil in the absence of a version' do
255
+ expect(metadata.update('name' => 'foo/bar').release_name).to be_nil
256
+ end
257
+
258
+ it 'returns a hyphenated string containing module name and version' do
259
+ data = metadata.update('name' => 'foo/bar', 'version' => '1.0.0')
260
+ expect(data.release_name).to eq('foo-bar-1.0.0')
261
+ end
262
+
263
+ it 'is unaffected by author name' do
264
+ data = metadata.update('name' => 'foo/bar', 'version' => '1.0.0', 'author' => 'me')
265
+ expect(data.release_name).to eq('foo-bar-1.0.0')
266
+ end
267
+ end
268
+
269
+ describe "#to_hash" do
270
+ subject { metadata.to_hash }
271
+
272
+ it "contains the default set of keys" do
273
+ expect(subject.keys.sort).to eq(%w[ name version author summary license source issues_url project_page dependencies ].sort)
274
+ end
275
+
276
+ describe "['license']" do
277
+ it "defaults to Apache 2" do
278
+ expect(subject['license']).to eq("Apache-2.0")
279
+ end
280
+ end
281
+
282
+ describe "['dependencies']" do
283
+ it "defaults to an empty set" do
284
+ expect(subject['dependencies']).to eq(Set.new)
285
+ end
286
+ end
287
+
288
+ context "when updated with non-default data" do
289
+ subject { metadata.update('license' => 'MIT', 'non-standard' => 'yup').to_hash }
290
+
291
+ it "overrides the defaults" do
292
+ expect(subject['license']).to eq('MIT')
293
+ end
294
+
295
+ it 'contains unanticipated values' do
296
+ expect(subject['non-standard']).to eq('yup')
297
+ end
298
+ end
299
+ end
300
+ end