visor-meta 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ require "spec_helper"
2
+
3
+ include Visor::Meta::Backends
4
+
5
+ module Visor::Meta::Backends
6
+ describe Base do
7
+
8
+ before(:each) do
9
+ @sample = {
10
+ name: 'testsample',
11
+ architecture: 'i386',
12
+ access: 'public',
13
+ format: 'iso'
14
+ }
15
+
16
+ @base = Base.new(host: 'fake', port: 'fake', db: 'fake')
17
+ end
18
+
19
+ describe "#initialize" do
20
+ it "should create an Base instance" do
21
+ base = Base.new(host: 'fake')
22
+ base.host.should == 'fake'
23
+ end
24
+ end
25
+
26
+ describe "#validate_data_post" do
27
+ it "should validate that no read-only field is setted" do
28
+ Base::READONLY.each do |field|
29
+ l = lambda { @base.validate_data_post @sample.merge(field => 'some value') }
30
+ l.should raise_error(ArgumentError, /#{field}/)
31
+ end
32
+ end
33
+
34
+ it "should validate that all mandatory fields are setted" do
35
+ Base::MANDATORY.each do |field|
36
+ l = lambda { @base.validate_data_post(@sample.select { |k, v| k != field }) }
37
+ l.should raise_error(ArgumentError, /#{field}/)
38
+ end
39
+ end
40
+
41
+ it "should validate fields values" do
42
+ fields = [:architecture, :access, :store, :format, :type]
43
+ fields.each do |field|
44
+ inv = @sample.merge(field => 'invalid value!')
45
+ l = lambda { @base.validate_data_post inv }
46
+ l.should raise_error(ArgumentError, /#{field}/)
47
+ end
48
+ end
49
+ end
50
+
51
+ describe "#validate_data_put" do
52
+ it "should validate that no read-only field is setted" do
53
+ Base::READONLY.each do |field|
54
+ l = lambda { @base.validate_data_put @sample.merge(field => 'some value') }
55
+ l.should raise_error(ArgumentError, /#{field}/)
56
+ end
57
+ end
58
+
59
+ it "should validate the the fields values" do
60
+ fields = [:architecture, :access, :store, :format, :type]
61
+ fields.each do |field|
62
+ inv = @sample.merge(field => 'invalid value!')
63
+ l = lambda { @base.validate_data_post inv }
64
+ l.should raise_error(ArgumentError, /#{field}/)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#validate_query_filters" do
70
+ it "should validate if query only contains valid filter keys" do
71
+ @base.validate_query_filters(Base::FILTERS.sample => 'value')
72
+ end
73
+
74
+ it "should raise if query has some invalid filter key" do
75
+ l = lambda { @base.validate_query_filters(invalid_filter: 'value') }
76
+ l.should raise_error(ArgumentError, /invalid_filter/)
77
+ end
78
+ end
79
+
80
+ describe "#set_protected_post" do
81
+ before(:each) do
82
+ @meta = {name: 'testsample',
83
+ architecture: 'i386',
84
+ format: 'iso',
85
+ owner: 'someone',
86
+ size: 1}
87
+ @base.set_protected_post @meta
88
+ end
89
+
90
+ it "should set the _id to a SecureRandom UUID" do
91
+ @meta[:_id].should be_a String
92
+ @meta[:_id].size.should == 36
93
+ end
94
+
95
+ it "should set the access to public if not provided" do
96
+ @meta[:access].should == 'public'
97
+ end
98
+
99
+ it "should set the created_at" do
100
+ @meta[:created_at].should be_a Time
101
+ end
102
+
103
+ it "should set the uri" do
104
+ @meta[:uri].should be_a String
105
+ @meta[:uri] =~ /#{@meta[:_id]}/
106
+ end
107
+
108
+ it "should set the status to locked" do
109
+ @meta[:status].should == 'locked'
110
+ end
111
+
112
+ it "should set the owner if provided" do
113
+ @meta[:owner].should == 'someone'
114
+ end
115
+
116
+ it "should set the size if provided" do
117
+ @meta[:size].should == 1
118
+ end
119
+ end
120
+
121
+ describe "#set_protected_put" do
122
+ it "should set the updated_at" do
123
+ meta = {name: 'new name',
124
+ architecture: 'i386'}
125
+ @base.set_protected_put meta
126
+ meta[:updated_at].should be_a Time
127
+ end
128
+ end
129
+
130
+ describe "#build_uri" do
131
+ it "should build a new URI" do
132
+ uri = @base.build_uri('some_id')
133
+ uri.should be_a String
134
+ uri =~ %r{^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/}ix
135
+ end
136
+ end
137
+
138
+ describe "#serialize_others" do
139
+ it "should encapsulate extra fields on the others field" do
140
+ sample = {name: 'example', access: 'public', extra_key: 'value', another: 'value'}
141
+ new = @base.serialize_others(sample)
142
+ new[:others].should == "{\"extra_key\":\"value\",\"another\":\"value\"}"
143
+ end
144
+ end
145
+
146
+ describe "#serialize_others" do
147
+ it "should encapsulate extra fields on the others field as JSON" do
148
+ sample = {name: 'example', access: 'public', extra_key: 'value', another: 'value'}
149
+ @base.serialize_others(sample)
150
+ sample[:others].should == "{\"extra_key\":\"value\",\"another\":\"value\"}"
151
+ end
152
+
153
+ it "should do nothing if there are no extra fields" do
154
+ sample = {name: 'example', access: 'public'}
155
+ copy = sample.dup
156
+ @base.serialize_others(sample)
157
+ sample.should == copy
158
+ end
159
+ end
160
+
161
+ describe "#deserialize_others" do
162
+ it "should decapsulate extra fields from the others field" do
163
+ sample = {name: 'example', access: 'public', extra_key: 'value', another: 'value'}
164
+ new = @base.deserialize_others(@base.serialize_others(sample))
165
+ new.should == sample
166
+ end
167
+
168
+ it "should do nothing if there are no extra fields" do
169
+ sample = {name: 'example', access: 'public'}
170
+ copy = sample.dup
171
+ @base.deserialize_others(sample)
172
+ sample.should == copy
173
+ end
174
+ end
175
+
176
+ describe "#string_time_or_hash?" do
177
+ it "should return true if parameter is a String a Time or a Hash" do
178
+ @base.string_time_or_hash?("").should be_true
179
+ @base.string_time_or_hash?(Time.now).should be_true
180
+ @base.string_time_or_hash?({}).should be_true
181
+ end
182
+
183
+ it "should return false if parameter is of other class" do
184
+ @base.string_time_or_hash?(1).should be_false
185
+ end
186
+ end
187
+
188
+ describe "#to_sql_where" do
189
+ it "should return a AND joined valid SQL WHERE string from a hash" do
190
+ str = @base.to_sql_where(a: 1, b: 'something')
191
+ str.should == "a=1 AND b='something'"
192
+ end
193
+ end
194
+
195
+ describe "#to_sql_update" do
196
+ it "should return a comma joined valid SQL UPDATE string from a hash" do
197
+ str = @base.to_sql_update(a: 1, b: 'something')
198
+ str.should == "a=1, b='something'"
199
+ end
200
+ end
201
+
202
+ describe "#to_sql_insert" do
203
+ it "should return a VALUES joined valid SQL INSERT array from a hash" do
204
+ arr = @base.to_sql_insert(a: 1, b: 'something')
205
+ arr.should == ["(a, b)", "(1, 'something')"]
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,152 @@
1
+ require "spec_helper"
2
+
3
+ include Visor::Common::Exception
4
+ include Visor::Meta::Backends
5
+
6
+ module Visor::Meta::Backends
7
+ describe MongoDB do
8
+
9
+ let(:conn) { MongoDB.connect uri: 'mongodb://:@127.0.0.1:27017/visor_test' }
10
+
11
+ before(:each) do
12
+ conn.post_image ({:name => 'testsample',
13
+ :architecture => 'i386',
14
+ :access => 'public',
15
+ :format => 'iso'})
16
+
17
+ @sample = {:name => 'xyz',
18
+ :architecture => 'x86_64',
19
+ :access => 'public',
20
+ :type => 'kernel'}
21
+ end
22
+
23
+ after(:all) do
24
+ conn.delete_all!
25
+ end
26
+
27
+ describe "#connect" do
28
+ it "should instantiate a new object trougth options" do
29
+ obj = conn
30
+ obj.db.should == 'visor_test'
31
+ obj.host.should == MongoDB::DEFAULT_HOST
32
+ end
33
+
34
+ it "should instantiate a new object trougth URI" do
35
+ uri = "mongodb://:@#{MongoDB::DEFAULT_HOST}:#{MongoDB::DEFAULT_PORT}/visor_test"
36
+ obj = MongoDB.connect uri: uri
37
+ obj.db.should == 'visor_test'
38
+ obj.host.should == MongoDB::DEFAULT_HOST
39
+ end
40
+ end
41
+
42
+ describe "#connection" do
43
+ it "should return a collection connection" do
44
+ conn.connection.should be_a Mongo::Collection
45
+ end
46
+ end
47
+
48
+ describe "#get_public_images" do
49
+ it "should return an array with all public images meta" do
50
+ pub = conn.get_public_images
51
+ pub.should be_a Array
52
+ pub.each { |img| img['access'].should == 'public' }
53
+ end
54
+
55
+ it "should return only brief information" do
56
+ pub = conn.get_public_images(true)
57
+ pub.each { |img| (img.keys & MongoDB::BRIEF).should be_empty }
58
+ end
59
+
60
+ it "should filter results if asked to" do
61
+ pub = conn.get_public_images(false, architecture: 'i386')
62
+ pub.each { |img| img['architecture'].should == 'i386' }
63
+ end
64
+
65
+ it "should sort results if asked to" do
66
+ conn.post_image @sample
67
+ pub = conn.get_public_images(false, sort: 'architecture', dir: 'desc')
68
+ pub.first['architecture'].should == 'x86_64'
69
+ end
70
+
71
+ it "should raise an NotFound exception if there are no public images" do
72
+ conn.delete_all!
73
+ lambda { conn.get_public_images }.should raise_error(NotFound, /public/)
74
+ end
75
+ end
76
+
77
+ describe "#get_image" do
78
+ before(:each) do
79
+ @id = conn.get_public_images.first['_id']
80
+ end
81
+
82
+ it "should return a bson hash with the asked image" do
83
+ img = conn.get_image(@id)
84
+ img.should be_a BSON::OrderedHash
85
+ img['_id'].should == @id
86
+ end
87
+
88
+ it "should return only detail information fields" do
89
+ img = conn.get_image(@id)
90
+ (img.keys & Base::DETAIL_EXC).should be_empty
91
+ end
92
+
93
+ it "should raise an NotFound exception if image not found" do
94
+ fake_id = 0
95
+ lambda { conn.get_image(fake_id) }.should raise_error(NotFound, /id/)
96
+ end
97
+ end
98
+
99
+ describe "#delete_image" do
100
+ it "should return a bson hash with the deleted image" do
101
+ id = conn.get_public_images.first['_id']
102
+ img = conn.delete_image(id)
103
+ img.should be_a BSON::OrderedHash
104
+ img['_id'].should == id
105
+ end
106
+
107
+ it "should raise an NotFound exception if image not found" do
108
+ fake_id = 0
109
+ lambda { conn.get_image(fake_id) }.should raise_error(NotFound, /id/)
110
+ end
111
+ end
112
+
113
+ describe "#delete_all!" do
114
+ it "should delete all records in images and counters collection" do
115
+ conn.delete_all!
116
+ conn.connection.find.to_a.should == []
117
+ end
118
+ end
119
+
120
+ describe "#post_image" do
121
+ it "should post an image and return its meta" do
122
+ image = conn.post_image(@sample)
123
+ image.should be_a(Hash)
124
+ image['name'].should == @sample[:name]
125
+ end
126
+
127
+ it "should raise an ArgumentError exception if meta validation fails" do
128
+ img = @sample.merge(:_id => 'cant define _id')
129
+ lambda { conn.post_image(img) }.should raise_error(ArgumentError, /_id/)
130
+ end
131
+ end
132
+
133
+ describe "#put_image" do
134
+ before(:each) do
135
+ @id = conn.get_public_images.first['_id']
136
+ end
137
+
138
+ it "should return a bson hash with updated image meta" do
139
+ update = {:name => 'updated', :type => 'machine'}
140
+ img = conn.put_image(@id, update)
141
+ img.should be_a BSON::OrderedHash
142
+ img['name'].should == 'updated'
143
+ img['type'].should == 'machine'
144
+ end
145
+
146
+ it "should raise an ArgumentError exception if meta validation fails" do
147
+ update = {:uri => 'cant define uri'}
148
+ lambda { conn.put_image(@id, update) }.should raise_error(ArgumentError, /uri/)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,164 @@
1
+ require "spec_helper"
2
+
3
+ include Visor::Common::Exception
4
+ include Visor::Meta::Backends
5
+
6
+ module Visor::Meta::Backends
7
+ describe MySQL do
8
+
9
+ let(:conn) { MySQL.connect uri: 'mysql://visor:passwd@127.0.0.1:3306/visor_test' }
10
+
11
+ before(:each) do
12
+ conn.post_image ({:name => 'testsample',
13
+ :architecture => 'i386',
14
+ :access => 'public',
15
+ :format => 'iso'})
16
+
17
+ @sample = {:name => 'xyz',
18
+ :architecture => 'x86_64',
19
+ :access => 'public',
20
+ :type => 'kernel'}
21
+ end
22
+
23
+ after(:all) do
24
+ conn.delete_all!
25
+ end
26
+
27
+ describe "#connect" do
28
+ it "should instantiate a new object trougth options" do
29
+ obj = conn
30
+ obj.db.should == 'visor_test'
31
+ obj.host.should == MySQL::DEFAULT_HOST
32
+ end
33
+ end
34
+
35
+ describe "#connection" do
36
+ it "should return a connection to the dabatase" do
37
+ conn.connection.should be_an_instance_of Mysql2::Client
38
+ end
39
+ end
40
+
41
+ describe "#get_public_images" do
42
+ it "should return an array with all public images meta" do
43
+ pub = conn.get_public_images
44
+ pub.should be_an_instance_of Array
45
+ pub.each { |img| img[:access].should == 'public' }
46
+ end
47
+
48
+ it "should return extra fields" do
49
+ conn.post_image(@sample.merge(extra_field: 'value'))
50
+ returned = 0
51
+ pub = conn.get_public_images
52
+ pub.each { |img| returned = 1 if img[:extra_field] }
53
+ returned.should == 1
54
+ end
55
+
56
+ it "should return only brief information" do
57
+ pub = conn.get_public_images(true)
58
+ pub.each { |img| (img.keys - Base::BRIEF).should be_empty }
59
+ end
60
+
61
+ it "should sort results if asked to" do
62
+ conn.post_image(@sample)
63
+ pub = conn.get_public_images(false, sort: 'architecture', dir: 'desc')
64
+ pub.first[:architecture].should == 'x86_64'
65
+ end
66
+
67
+ it "should raise an NotFound exception if there are no public images" do
68
+ conn.delete_all!
69
+ lambda { conn.get_public_images }.should raise_error(NotFound, /public/)
70
+ end
71
+ end
72
+
73
+ describe "#get_image" do
74
+ before(:each) do
75
+ @id = conn.get_public_images.first[:_id]
76
+ end
77
+
78
+ it "should return a hash with the asked image meta" do
79
+ img = conn.get_image(@id)
80
+ img.should be_a(Hash)
81
+ img[:_id].should == @id
82
+ end
83
+
84
+ it "should return only detail information fields" do
85
+ img = conn.get_image(@id)
86
+ (img.keys & Base::DETAIL_EXC).should be_empty
87
+ end
88
+
89
+ it "should return extra fields" do
90
+ image = conn.post_image(@sample.merge(extra_field: 'value'))
91
+ image[:extra_field].should == 'value'
92
+ end
93
+
94
+ it "should raise an NotFound exception if image not found" do
95
+ fake_id = 0
96
+ lambda { conn.get_image(fake_id) }.should raise_error(NotFound, /id/)
97
+ end
98
+ end
99
+
100
+ describe "#delete_image" do
101
+ it "should return a hash with the deleted image meta" do
102
+ id = conn.get_public_images.first[:_id]
103
+ img = conn.delete_image(id)
104
+ img.should be_a(Hash)
105
+ img[:_id].should == id
106
+ end
107
+
108
+ it "should raise an exception if image not found" do
109
+ fake_id = 0
110
+ lambda { conn.delete_image(fake_id) }.should raise_error(NotFound, /id/)
111
+ end
112
+ end
113
+
114
+ describe "#delete_all!" do
115
+ it "should delete all records in images and counters collection" do
116
+ conn.delete_all!
117
+ lambda { conn.get_public_images }.should raise_error(NotFound, /public/)
118
+ end
119
+ end
120
+
121
+ describe "#post_image" do
122
+ it "should post an image and return it" do
123
+ image = conn.post_image(@sample, method: 1)
124
+ image.should be_a(Hash)
125
+ image[:name].should == @sample[:name]
126
+ end
127
+
128
+ it "should post an image with additional fields" do
129
+ image = conn.post_image(@sample.merge(extra_field: 'value'))
130
+ image[:extra_field].should == 'value'
131
+ end
132
+
133
+ it "should raise an exception if meta validation fails" do
134
+ img = @sample.merge(:_id => '_id can not be set')
135
+ lambda { conn.post_image(img) }.should raise_error(ArgumentError, /_id/)
136
+ end
137
+ end
138
+
139
+ describe "#put_image" do
140
+ before(:each) do
141
+ @id = conn.get_public_images.first[:_id]
142
+ end
143
+
144
+ it "should return a hash with updated image" do
145
+ update = {:name => 'updated', :type => 'kernel'}
146
+ img = conn.put_image(@id, update)
147
+ img.should be_a(Hash)
148
+ img[:name].should == 'updated'
149
+ img[:type].should == 'kernel'
150
+ end
151
+
152
+ it "should update extra fields too" do
153
+ id = conn.post_image(@sample.merge(extra_field: 'value'))[:_id]
154
+ image = conn.put_image(id, extra_field: 'new value')
155
+ image[:extra_field].should == 'new value'
156
+ end
157
+
158
+ it "should raise an exception if meta validation fails" do
159
+ update = {:uri => 'uri can not be set'}
160
+ lambda { conn.put_image(@id, update) }.should raise_error(ArgumentError, /uri/)
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,179 @@
1
+ require "spec_helper"
2
+
3
+ module Visor::Meta
4
+ describe Client do
5
+
6
+ include Visor::Meta
7
+ include Visor::Common::Exception
8
+
9
+ let(:client) { Client.new }
10
+ let(:not_found) { Visor::Common::Exception::NotFound }
11
+ let(:invalid) { Visor::Common::Exception::Invalid }
12
+
13
+ let(:valid_post) { {name: 'client_spec', architecture: 'i386', access: 'public'} }
14
+ let(:invalid_post) { {name: 'client_spec', architecture: 'i386', access: 'invalid'} }
15
+
16
+ let(:valid_update) { {architecture: 'x86_64'} }
17
+ let(:invalid_update) { {architecture: 'invalid'} }
18
+
19
+ inserted = []
20
+
21
+ before(:all) do
22
+ inserted << client.post_image(valid_post)[:_id]
23
+ inserted << client.post_image(valid_post.merge(architecture: 'x86_64'))[:_id]
24
+ end
25
+
26
+ after(:all) do
27
+ inserted.each { |id| client.delete_image(id) }
28
+ end
29
+
30
+ describe "#initialize" do
31
+ it "should instantiate a new client with default options" do
32
+ client.host.should == Client::DEFAULT_HOST
33
+ client.port.should == Client::DEFAULT_PORT
34
+ client.ssl.should be_false
35
+ end
36
+
37
+ it "should instantiate a new client with provided options" do
38
+ c = Visor::Meta::Client.new(host: '1.1.1.1', port: 1, ssl: true)
39
+ c.host.should == '1.1.1.1'
40
+ c.port.should == 1
41
+ c.ssl.should be_true
42
+ end
43
+ end
44
+
45
+ describe "#get_images" do
46
+ before(:each) do
47
+ @images = client.get_images
48
+ end
49
+
50
+ it "should return an array" do
51
+ @images.should be_a Array
52
+ end
53
+
54
+ it "should filter results if asked to" do
55
+ pub = client.get_images(architecture: 'x86_64')
56
+ pub.each { |img| img[:architecture].should == 'x86_64' }
57
+ end
58
+
59
+ it "should sort results if asked to" do
60
+ pub = client.get_images(sort: 'architecture', dir: 'desc')
61
+ pub.first[:architecture].should == 'x86_64'
62
+ pub = client.get_images(sort: 'architecture', dir: 'asc')
63
+ pub.first[:architecture].should == 'i386'
64
+ end
65
+ end
66
+
67
+ describe "#get_images_detail" do
68
+ before(:each) do
69
+ @images = client.get_images_detail
70
+ end
71
+
72
+ it "should return an array" do
73
+ @images.should be_a Array
74
+ end
75
+
76
+ it "should return all public images" do
77
+ @images.each { |image| image[:access].should == 'public' }
78
+ end
79
+
80
+ it "should filter results if asked to" do
81
+ pub = client.get_images_detail(architecture: 'x86_64')
82
+ pub.each { |img| img[:architecture].should == 'x86_64' }
83
+ end
84
+
85
+ it "should sort results if asked to" do
86
+ pub = client.get_images(sort: 'architecture', dir: 'desc')
87
+ pub.first[:architecture].should == 'x86_64'
88
+ pub = client.get_images(sort: 'architecture', dir: 'asc')
89
+ pub.first[:architecture].should == 'i386'
90
+ end
91
+ end
92
+
93
+ describe "#get_image" do
94
+ before(:each) do
95
+ @id = client.post_image(valid_post)[:_id]
96
+ inserted << @id
97
+ @image = client.get_image(@id)
98
+ end
99
+
100
+ it "should return a hash" do
101
+ @image.should be_a Hash
102
+ end
103
+
104
+ it "should return the asked image metadata" do
105
+ @image[:_id].should == @id
106
+ end
107
+
108
+ it "should raise an exception if image not found" do
109
+ fake_id = 0
110
+ lambda { client.get_image(fake_id) }.should raise_error not_found
111
+ end
112
+ end
113
+
114
+ describe "#delete_image" do
115
+ before(:each) do
116
+ @id = client.post_image(valid_post)[:_id]
117
+ @image = client.delete_image(@id)
118
+ end
119
+
120
+ it "should return a hash" do
121
+ @image.should be_a Hash
122
+ end
123
+
124
+ it "should return the deleted image metadata" do
125
+ @image[:_id].should == @id
126
+ end
127
+
128
+ it "should trully delete that image from database" do
129
+ lambda { client.get_image(@id) }.should raise_error not_found
130
+ end
131
+
132
+ it "should raise an exception if image not found" do
133
+ fake_id = 0
134
+ lambda { client.delete_image(fake_id) }.should raise_error not_found
135
+ end
136
+ end
137
+
138
+ describe "#post_image" do
139
+ before(:each) do
140
+ @image = client.post_image(valid_post)
141
+ inserted << @image[:_id]
142
+ end
143
+
144
+ it "should return a hash" do
145
+ @image.should be_a Hash
146
+ end
147
+
148
+ it "should return posted image metadata" do
149
+ @image[:_id].should be_a(String)
150
+ @image[:access].should == valid_post[:access]
151
+ end
152
+
153
+ it "should raise an exception if meta validation fails" do
154
+ lambda { client.post_image(invalid_post) }.should raise_error invalid
155
+ end
156
+ end
157
+
158
+ describe "#put_image" do
159
+ before :each do
160
+ @id = client.post_image(valid_post)[:_id]
161
+ inserted << @id
162
+ @image = client.put_image(@id, valid_update)
163
+ end
164
+
165
+ it "should return a hash" do
166
+ @image.should be_a Hash
167
+ end
168
+
169
+ it "should return update image metadata" do
170
+ @image[:_id].should == @id
171
+ @image[:architecture].should == valid_update[:architecture]
172
+ end
173
+
174
+ it "should raise an exception if meta validation fails" do
175
+ lambda { client.put_image(@id, invalid_update) }.should raise_error invalid
176
+ end
177
+ end
178
+ end
179
+ end