af-addon-tester 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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +45 -0
- data/Rakefile +1 -0
- data/af-addon-tester.gemspec +23 -0
- data/bin/af-addon-tester +183 -0
- data/config/manifest.example.json +16 -0
- data/lib/af-addon-tester/colorize.rb +58 -0
- data/lib/af-addon-tester/rest.rb +106 -0
- data/lib/af-addon-tester/test.rb +22 -0
- data/lib/af-addon-tester/version.rb +5 -0
- data/lib/af-addon-tester.rb +4 -0
- metadata +85 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
af-addon-tester
|
2
|
+
===============
|
3
|
+
|
4
|
+
<img src="http://appfog.com/images/logo.png" />
|
5
|
+
|
6
|
+
Allows developers to test App Fog add-ons
|
7
|
+
|
8
|
+
## Setup ##
|
9
|
+
|
10
|
+
1) Clone
|
11
|
+
|
12
|
+
$ git clone git@github.com:tsantef/af-addon-tester.git
|
13
|
+
|
14
|
+
2) Create a manifest.json that points to a test addon
|
15
|
+
|
16
|
+
Example
|
17
|
+
|
18
|
+
{
|
19
|
+
"id":"myaddon",
|
20
|
+
"api":{
|
21
|
+
"plans":[
|
22
|
+
{"id":"free"}
|
23
|
+
],
|
24
|
+
"config_vars": {
|
25
|
+
"MYADDON_URL":"http://some.url.com",
|
26
|
+
"MYADDON_VAR1":"cats",
|
27
|
+
"MYADDON_VAR2":"dogs"
|
28
|
+
},
|
29
|
+
"test":"http://localhost:4567/myaddon/resources",
|
30
|
+
"password":"cavef6azebRewruvecuch",
|
31
|
+
"sso_salt":"8ouy3ayLEyOA7HLAKO2Yo"
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
|
36
|
+
## Usage ##
|
37
|
+
|
38
|
+
$ af-addon-tester <path to manifest>
|
39
|
+
|
40
|
+
|
41
|
+
## Meta ##
|
42
|
+
|
43
|
+
Maintained by Tim Santeford.
|
44
|
+
|
45
|
+
Released under the MIT license.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "af-addon-tester/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "af-addon-tester"
|
7
|
+
s.version = AppFog::AddonTester::VERSION
|
8
|
+
s.default_executable = %q{af-addon-tester}
|
9
|
+
s.authors = ["Tim Santeford"]
|
10
|
+
s.email = ["tim@phpfog.com"]
|
11
|
+
s.homepage = "http://www.appfog.com"
|
12
|
+
s.summary = %q{Tests App Fog Add-ons}
|
13
|
+
s.description = %q{Allows developers to test App Fog add-ons}
|
14
|
+
|
15
|
+
s.rubyforge_project = "af-addon-tester"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_runtime_dependency 'json'
|
23
|
+
end
|
data/bin/af-addon-tester
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
|
4
|
+
$LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'json'
|
8
|
+
require 'digest/sha1'
|
9
|
+
require 'af-addon-tester'
|
10
|
+
|
11
|
+
manifest_path = ARGV.first
|
12
|
+
|
13
|
+
begin
|
14
|
+
manifest_file = File.open(File.expand_path(manifest_path), 'r')
|
15
|
+
manifest_json = manifest_file.readlines.to_s
|
16
|
+
manifest = JSON.parse(manifest_json)
|
17
|
+
|
18
|
+
raise "Missing id" if manifest['id'].nil?
|
19
|
+
raise "Missing api section" if manifest['api'].nil?
|
20
|
+
raise "Missing api password" if manifest['api']['password'].nil?
|
21
|
+
raise "Missing apt test url" if manifest['api']['test'].nil?
|
22
|
+
raise "Manifest must have atleast one plan" if manifest['api']['plans'].nil? || manifest['api']['plans'][0].nil? || manifest['api']['plans'][0]['id'].nil?
|
23
|
+
raise "missing api config_vars" if manifest['api']['config_vars'].nil?
|
24
|
+
raise "Missing api sso_salt" if manifest['api']['sso_salt'].nil?
|
25
|
+
|
26
|
+
rescue Exception => e
|
27
|
+
puts bwhite "#{e.message}. Please specify a valid manifest file."
|
28
|
+
exit
|
29
|
+
end
|
30
|
+
|
31
|
+
callback_url = 'http://localhost:9990'
|
32
|
+
config_prefix = manifest['id'].gsub('-','_').upcase + '_'
|
33
|
+
bad_user = 'bad_user'
|
34
|
+
bad_password = 'bad_pass'
|
35
|
+
|
36
|
+
addon = Rest.new(manifest['api']['test'], manifest['id'], manifest['api']['password'])
|
37
|
+
resp = nil
|
38
|
+
|
39
|
+
validate "Provisioning" do
|
40
|
+
params = {}
|
41
|
+
payload = { 'customer_id' => manifest['id'], 'plan' => manifest['api']['plans'][0]['id'], 'callback_url' => callback_url, 'options' => '{}' }
|
42
|
+
resp = addon.post(manifest['api']['test'], params, JSON.generate(payload))
|
43
|
+
failed("response code: #{resp.code} - #{resp.message}") if resp.code != "200"
|
44
|
+
passed
|
45
|
+
end
|
46
|
+
|
47
|
+
if resp.code == "200"
|
48
|
+
provision_info = nil
|
49
|
+
validate "Valid JSON response" do
|
50
|
+
begin
|
51
|
+
provision_info = JSON.parse(resp.body)
|
52
|
+
rescue Exception => e
|
53
|
+
failed e.message
|
54
|
+
end
|
55
|
+
passed
|
56
|
+
end
|
57
|
+
|
58
|
+
unless provision_info.nil?
|
59
|
+
validate "Response params" do
|
60
|
+
begin
|
61
|
+
failed("Missing 'id'") if provision_info['id'].nil? || provision_info['id'] == ""
|
62
|
+
rescue Exception => e
|
63
|
+
failed e.message
|
64
|
+
end
|
65
|
+
passed
|
66
|
+
end
|
67
|
+
|
68
|
+
unless provision_info['config'].nil?
|
69
|
+
validate "All config keys are in manifest" do
|
70
|
+
provision_info['config'].each do |key, value|
|
71
|
+
unless manifest['api']['config_vars'].include? key; failed "#{key} not found in config"; end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
validate "All manifest config keys are in response" do
|
76
|
+
manifest['api']['config_vars'].each do |key, value|
|
77
|
+
unless provision_info['config'].include? key; failed "#{key} not found in manifest"; end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
validate "All config keys are prefixed with addon name" do
|
82
|
+
provision_info['config'].each do |key, value|
|
83
|
+
unless key.start_with? config_prefix; failed "#{key} does not begin with #{config_prefix}"; end
|
84
|
+
end
|
85
|
+
passed
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
resource_id = provision_info['id']
|
90
|
+
unless resource_id.nil?
|
91
|
+
validate "Update resource" do
|
92
|
+
params = {}
|
93
|
+
payload = { 'plan' => 'paid', 'callback_url' => callback_url, 'options' => '{}' }
|
94
|
+
resp = addon.put(manifest['api']['test'] + "/#{resource_id}", params, JSON.generate(payload))
|
95
|
+
failed("response code: #{resp.code}") if resp.code != "200"
|
96
|
+
passed
|
97
|
+
end
|
98
|
+
|
99
|
+
READLEN = 1024 * 10
|
100
|
+
reader, writer = IO.pipe
|
101
|
+
out = nil
|
102
|
+
validate "Callback" do
|
103
|
+
child = fork do
|
104
|
+
reader.close
|
105
|
+
server = TCPServer.open(9990)
|
106
|
+
client = server.accept
|
107
|
+
writer.write(client.readpartial(READLEN))
|
108
|
+
client.write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
|
109
|
+
client.close
|
110
|
+
writer.close
|
111
|
+
end
|
112
|
+
sleep(1)
|
113
|
+
out = reader.readpartial(READLEN)
|
114
|
+
passed
|
115
|
+
end
|
116
|
+
|
117
|
+
callback_info = nil
|
118
|
+
isValidCallback = false
|
119
|
+
validate "Valid callback response" do
|
120
|
+
_, json = out.split("\r\n\r\n")
|
121
|
+
begin
|
122
|
+
callback_info = JSON.parse(json)
|
123
|
+
rescue Exception => e
|
124
|
+
failed e.message
|
125
|
+
end
|
126
|
+
isValidCallback = true
|
127
|
+
passed
|
128
|
+
end
|
129
|
+
|
130
|
+
if isValidCallback
|
131
|
+
validate "All callback config keys are in manifest" do
|
132
|
+
callback_info['config'].each do |key, value|
|
133
|
+
unless manifest['api']['config_vars'].include? key; failed "#{key} not found in config"; end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
validate "All callback manifest config keys are in response" do
|
138
|
+
manifest['api']['config_vars'].each do |key, value|
|
139
|
+
unless callback_info['config'].include? key; failed "#{key} not found in manifest"; end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
validate "All callback config keys are prefixed with addon name" do
|
144
|
+
callback_info['config'].each do |key, value|
|
145
|
+
unless key.start_with? config_prefix; failed "#{key} does not begin with #{config_prefix}"; end
|
146
|
+
end
|
147
|
+
passed
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
validate "SSO link" do
|
152
|
+
timestamp = Time.now.to_i
|
153
|
+
authstring = resource_id.to_s + ':' + manifest['api']['sso_salt'] + ':' + timestamp.to_s
|
154
|
+
token = Digest::SHA1.hexdigest(authstring)
|
155
|
+
resp = addon.get(manifest['api']['test'] + "/#{resource_id}?token=#{token}×tamp=#{timestamp}")
|
156
|
+
passed unless resp.code != "200"
|
157
|
+
end
|
158
|
+
|
159
|
+
validate "Deprovision" do
|
160
|
+
params = {}
|
161
|
+
payload = { 'customer_id' => manifest['id'], 'plan' => manifest['api']['plans'][0]['id'], 'callback_url' => callback_url, 'options' => '{}' }
|
162
|
+
resp = addon.delete(manifest['api']['test'] + "/#{resource_id}", params, JSON.generate(payload))
|
163
|
+
failed("response code: #{resp.code}") if resp.code != "200"
|
164
|
+
passed
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
else
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
validate "Bad credentials test" do
|
175
|
+
params = {}
|
176
|
+
payload = { 'customer_id' => manifest['id'], 'plan' => manifest['api']['plans'][0]['id'], 'callback_url' => callback_url, 'options' => '{}' }
|
177
|
+
|
178
|
+
addon_bad_auth = Rest.new(manifest['api']['test'], bad_user, bad_password)
|
179
|
+
resp = addon.post(manifest['api']['test'], params, JSON.generate(payload))
|
180
|
+
|
181
|
+
failed if resp.code == "200"
|
182
|
+
passed
|
183
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"id":"myaddon",
|
3
|
+
"api":{
|
4
|
+
"plans":[
|
5
|
+
{"id":"free"}
|
6
|
+
],
|
7
|
+
"config_vars": {
|
8
|
+
"MYADDON_URL":"http://some.url.com",
|
9
|
+
"MYADDON_VAR1":"cats",
|
10
|
+
"MYADDON_VAR2":"dogs"
|
11
|
+
},
|
12
|
+
"test":"http://localhost:4567/myaddon/resources",
|
13
|
+
"password":"cavef6azebRewruvecuch",
|
14
|
+
"sso_salt":"8ouy3ayLEyOA7HLAKO2Yo"
|
15
|
+
}
|
16
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
def colorize(str, beginColor, endColor = 0)
|
2
|
+
"\e[#{beginColor}m#{str}\e[#{endColor}m"
|
3
|
+
end
|
4
|
+
|
5
|
+
#30 Black
|
6
|
+
def black(str, endColor = 0)
|
7
|
+
colorize(str, "30", endColor)
|
8
|
+
end
|
9
|
+
|
10
|
+
#31 Red
|
11
|
+
def red(str, endColor = 0)
|
12
|
+
colorize(str, "31", endColor)
|
13
|
+
end
|
14
|
+
|
15
|
+
#32 Green
|
16
|
+
def green(str, endColor = 0)
|
17
|
+
colorize(str, "32", endColor)
|
18
|
+
end
|
19
|
+
|
20
|
+
#32 Bright Green
|
21
|
+
def bgreen(str, endColor = 0)
|
22
|
+
colorize(str, "1;32", endColor)
|
23
|
+
end
|
24
|
+
|
25
|
+
#33 Yellow
|
26
|
+
def yellow(str, endColor = 0)
|
27
|
+
colorize(str, "33", endColor)
|
28
|
+
end
|
29
|
+
|
30
|
+
#34 Blue
|
31
|
+
def blue(str, endColor = 0)
|
32
|
+
colorize(str, "34", endColor)
|
33
|
+
end
|
34
|
+
|
35
|
+
#35 Magenta
|
36
|
+
def magenta(str, endColor = 0)
|
37
|
+
colorize(str, "35", endColor)
|
38
|
+
end
|
39
|
+
|
40
|
+
#36 Cyan
|
41
|
+
def cyan(str, endColor = 0)
|
42
|
+
colorize(str, "36", endColor)
|
43
|
+
end
|
44
|
+
|
45
|
+
#36 Bright Cyan
|
46
|
+
def bcyan(str, endColor = 0)
|
47
|
+
colorize(str, "1;36", endColor)
|
48
|
+
end
|
49
|
+
|
50
|
+
#37 White
|
51
|
+
def white(str, endColor = 0)
|
52
|
+
colorize(str, "37", endColor)
|
53
|
+
end
|
54
|
+
|
55
|
+
#37 Bright White
|
56
|
+
def bwhite(str, endColor = 0)
|
57
|
+
colorize(str, "1;37", endColor)
|
58
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'net/https'
|
3
|
+
require 'uri'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
class Rest
|
7
|
+
|
8
|
+
$user = nil
|
9
|
+
$password = nil
|
10
|
+
$http = nil
|
11
|
+
$cookies = {}
|
12
|
+
$last_resp = nil
|
13
|
+
$last_params = nil
|
14
|
+
|
15
|
+
$useragent = ''
|
16
|
+
|
17
|
+
def initialize(url, user = nil, password = nil)
|
18
|
+
$user = user
|
19
|
+
$password = password
|
20
|
+
|
21
|
+
uri = URI(url)
|
22
|
+
$http = Net::HTTP.new(uri.host, uri.port)
|
23
|
+
|
24
|
+
if uri.scheme == 'https'
|
25
|
+
$http.use_ssl = true
|
26
|
+
$http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
27
|
+
else
|
28
|
+
$http.use_ssl = false
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def get(url, params = nil, additional_header = nil)
|
33
|
+
header = { 'Cookie' => cookie_to_s, 'User-Agent' => $useragent }
|
34
|
+
header = header.merge(additional_header) unless additional_header.nil?
|
35
|
+
req = Net::HTTP::Get.new(url, header)
|
36
|
+
make_request(req, params)
|
37
|
+
end
|
38
|
+
|
39
|
+
def post(url, params, payload = nil, additional_header = nil)
|
40
|
+
header = { 'Cookie' => cookie_to_s, 'User-Agent' => $useragent }
|
41
|
+
header = header.merge(additional_header) unless additional_header.nil?
|
42
|
+
req = Net::HTTP::Post.new(url, { 'Cookie' => cookie_to_s, 'User-Agent' => $useragent })
|
43
|
+
make_request(req, params, payload)
|
44
|
+
end
|
45
|
+
|
46
|
+
def put(url, params, payload = nil)
|
47
|
+
req = Net::HTTP::Put.new(url, { 'Cookie' => cookie_to_s, 'User-Agent' => $useragent })
|
48
|
+
make_request(req, params, payload)
|
49
|
+
end
|
50
|
+
|
51
|
+
def delete(url, params = nil, payload = nil)
|
52
|
+
req = Net::HTTP::Delete.new(url, { 'Cookie' => cookie_to_s, 'User-Agent' => $useragent })
|
53
|
+
make_request(req, params, payload)
|
54
|
+
end
|
55
|
+
|
56
|
+
def cookies
|
57
|
+
$cookies
|
58
|
+
end
|
59
|
+
def cookies=(dough)
|
60
|
+
$cookies = dough
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
puts "#{bwhite(resp.code)} - #{$last_resp.message}"
|
65
|
+
puts $last_params.inspect
|
66
|
+
puts "Cookies: " + $cookies.inspect
|
67
|
+
puts $last_resp.body
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def cookie_to_s
|
73
|
+
cookiestr = ''
|
74
|
+
$cookies.each do |key, value|
|
75
|
+
cookiestr += "#{key}=#{value}, "
|
76
|
+
end
|
77
|
+
cookiestr[0..-2]
|
78
|
+
end
|
79
|
+
|
80
|
+
def make_request(req, params = nil, payload = nil)
|
81
|
+
$last_params = params
|
82
|
+
req.basic_auth($user, $password) unless $user.nil?
|
83
|
+
req.set_form_data(params, ';') unless params.nil?
|
84
|
+
|
85
|
+
unless payload.nil?
|
86
|
+
req.body = payload
|
87
|
+
req.set_content_type('multipart/form-data')
|
88
|
+
end
|
89
|
+
|
90
|
+
begin
|
91
|
+
$last_resp = $http.request(req)
|
92
|
+
|
93
|
+
unless $last_resp['set-cookie'].nil?
|
94
|
+
$last_resp['set-cookie'].split(', ').each do |cookie|
|
95
|
+
key, value = cookie.split('=')
|
96
|
+
$cookies[key] = value
|
97
|
+
end
|
98
|
+
end
|
99
|
+
rescue NoMethodError
|
100
|
+
return Net::HTTPInternalServerError.new '1.1', '500', 'Internal server error'
|
101
|
+
end
|
102
|
+
|
103
|
+
$last_resp
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class ValidationFailed < StandardError ; end
|
2
|
+
|
3
|
+
def validate(msg)
|
4
|
+
print "#{msg}: "
|
5
|
+
begin
|
6
|
+
if yield
|
7
|
+
puts "[ " + bgreen("Pass") + " ]"
|
8
|
+
else
|
9
|
+
raise ValidationFailed, ''
|
10
|
+
end
|
11
|
+
rescue ValidationFailed => fail
|
12
|
+
puts "[ " + red("Failed") + " ] " + bwhite(fail.message)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def passed(msg = nil)
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
def failed(msg = '')
|
21
|
+
raise ValidationFailed, msg
|
22
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: af-addon-tester
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Tim Santeford
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2012-01-12 00:00:00 -08:00
|
18
|
+
default_executable: af-addon-tester
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: json
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
version: "0"
|
30
|
+
type: :runtime
|
31
|
+
version_requirements: *id001
|
32
|
+
description: Allows developers to test App Fog add-ons
|
33
|
+
email:
|
34
|
+
- tim@phpfog.com
|
35
|
+
executables:
|
36
|
+
- af-addon-tester
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- .gitignore
|
43
|
+
- Gemfile
|
44
|
+
- README.md
|
45
|
+
- Rakefile
|
46
|
+
- af-addon-tester.gemspec
|
47
|
+
- bin/af-addon-tester
|
48
|
+
- config/manifest.example.json
|
49
|
+
- lib/af-addon-tester.rb
|
50
|
+
- lib/af-addon-tester/colorize.rb
|
51
|
+
- lib/af-addon-tester/rest.rb
|
52
|
+
- lib/af-addon-tester/test.rb
|
53
|
+
- lib/af-addon-tester/version.rb
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://www.appfog.com
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project: af-addon-tester
|
80
|
+
rubygems_version: 1.3.6
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Tests App Fog Add-ons
|
84
|
+
test_files: []
|
85
|
+
|