esgob 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +2 -2
- data/bin/esgob +8 -0
- data/esgob.gemspec +13 -11
- data/lib/esgob/cli.rb +134 -0
- data/lib/esgob/client.rb +89 -25
- data/lib/esgob/version.rb +1 -1
- data/lib/esgob.rb +11 -1
- data/test/esgob_cli_test.rb +129 -0
- data/test/esgob_client_test.rb +118 -29
- data/test/esgob_version_test.rb +0 -2
- data/test/fixtures/code_1003.json +6 -0
- data/test/fixtures/code_2007.json +6 -0
- data/test/fixtures/sync-domain-list.txt +2 -0
- data/test/test_helper.rb +21 -4
- metadata +43 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f029cb699d12ea2f9ebc3d62538b301ac22c4ce2
|
4
|
+
data.tar.gz: 878e694007c2d41a71ed940fc5f7cf9215a4ad23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48559b9d9ac0b9a5def0e60cd6ed4242d90e28a2ec617d71fb673c0fedadd4e6aee7a32a81c6db88d649d539a59df04ab8e83e8317c5bfe65f442a13929159f5
|
7
|
+
data.tar.gz: 5ff773471a48fc33e975ae314aec3519f9d7422b8526301bebf66f5328955f886bf9c9a8d0fb03838766d2321df230988108810aca80091a00a9d17079945202
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -52,12 +52,12 @@ Here is an example of what can be done in an IRB session:
|
|
52
52
|
|
53
53
|
$ irb -resgob
|
54
54
|
irb(main):001:0> esgob = Esgob::Client.new
|
55
|
-
=> #<Esgob::Client
|
55
|
+
=> #<Esgob::Client account=myacct>
|
56
56
|
irb(main):002:0> esgob.domains_slaves_list
|
57
57
|
=> {"example.com"=>"192.168.0.1", "example.uk"=>"192.168.0.1"}
|
58
58
|
irb(main):003:0> esgob.domains_slaves_list.keys
|
59
59
|
=> ["example.com", "example.uk"]
|
60
|
-
irb(main):004:0>
|
60
|
+
irb(main):004:0>
|
61
61
|
|
62
62
|
See the [API documentation] for full details.
|
63
63
|
|
data/bin/esgob
ADDED
data/esgob.gemspec
CHANGED
@@ -4,35 +4,37 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'esgob/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'esgob'
|
8
8
|
spec.version = Esgob::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
#spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
9
|
+
spec.authors = ['Nicholas Humfrey']
|
10
|
+
spec.email = ['njh@aelius.com']
|
11
|
+
spec.summary = 'Client library for talking to the Esgob anycast DNS API.'
|
12
|
+
# spec.description = 'TODO: Write a longer description. Optional.'
|
13
|
+
spec.homepage = 'http://github.com/njh/ruby-esgob'
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
-
spec.require_paths = [
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
|
22
|
-
spec.add_dependency
|
21
|
+
spec.add_dependency 'json', '~> 1.8'
|
22
|
+
spec.add_dependency 'thor', '~> 0.19.1'
|
23
23
|
|
24
24
|
if Gem.ruby_version > Gem::Version.new('1.9')
|
25
25
|
spec.add_development_dependency 'bundler', '>= 1.5.0'
|
26
26
|
spec.add_development_dependency 'rake', '>= 0.10.0'
|
27
27
|
spec.add_development_dependency 'yard', '>= 0.8.0'
|
28
28
|
spec.add_development_dependency 'fakeweb', '~> 1.3.0'
|
29
|
+
spec.add_development_dependency 'mocha', '~> 1.1.0'
|
29
30
|
spec.add_development_dependency 'simplecov'
|
30
31
|
elsif Gem.ruby_version > Gem::Version.new('1.8')
|
31
32
|
spec.add_development_dependency 'bundler', '>= 1.1.0'
|
32
33
|
spec.add_development_dependency 'rake', '~> 0.9.0'
|
33
34
|
spec.add_development_dependency 'yard', '~> 0.8.0'
|
34
|
-
spec.add_development_dependency 'minitest', '~> 5.5.0'
|
35
35
|
spec.add_development_dependency 'fakeweb', '~> 1.3.0'
|
36
|
+
spec.add_development_dependency 'minitest', '~> 5.5.0'
|
37
|
+
spec.add_development_dependency 'mocha', '~> 1.1.0'
|
36
38
|
else
|
37
39
|
raise "#{Gem.ruby_version} is an unsupported version of ruby"
|
38
40
|
end
|
data/lib/esgob/cli.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
class Esgob::CLI < Thor
|
4
|
+
class_option :account,
|
5
|
+
:type => :string,
|
6
|
+
:aliases => '-a',
|
7
|
+
:banner => 'Account Name'
|
8
|
+
|
9
|
+
class_option :key,
|
10
|
+
:type => :string,
|
11
|
+
:aliases => '-k',
|
12
|
+
:banner => 'API Key'
|
13
|
+
|
14
|
+
class_option :verbose,
|
15
|
+
:type => :boolean,
|
16
|
+
:default => false,
|
17
|
+
:aliases => '-v'
|
18
|
+
|
19
|
+
def self.start(args = ARGV, config = {})
|
20
|
+
config[:shell] ||= Thor::Base.shell.new
|
21
|
+
begin
|
22
|
+
super(args, config)
|
23
|
+
rescue Esgob::ServerError => err
|
24
|
+
$stderr.puts config[:shell].set_color("=> Error: #{err.message} [#{err.code}]", :red, :bold)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "account", "Display account info"
|
29
|
+
def account
|
30
|
+
client.accounts_get.each_pair do |k, v|
|
31
|
+
say sprintf("%8s: %s\n", k, v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "domains", "List all domains"
|
36
|
+
def domains
|
37
|
+
print_table(
|
38
|
+
[['Domain', 'Type']] +
|
39
|
+
[['------', '----']] +
|
40
|
+
client.domains_list.map { |h| [h[:domain], h[:type]] }
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "slaves", "List slave domains"
|
45
|
+
def slaves
|
46
|
+
print_table(
|
47
|
+
[['Domain', 'Master IP']] +
|
48
|
+
[['------', '---------']] +
|
49
|
+
client.domains_slaves_list.to_a
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
desc "slaves-add DOMAIN MASTERIP", "Add new slave domain"
|
54
|
+
def slaves_add(domain, masterip)
|
55
|
+
check_action do
|
56
|
+
client.domains_slaves_add(domain, masterip)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
desc "slaves-delete DOMAIN", "Delete a slave domain"
|
61
|
+
def slaves_delete(domain)
|
62
|
+
check_action do
|
63
|
+
client.domains_slaves_delete(domain)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
desc "slaves-transfer DOMAIN",
|
68
|
+
"Force transfer from master of a slave domain"
|
69
|
+
def slaves_transfer(domain)
|
70
|
+
check_action do
|
71
|
+
client.domains_slaves_forcetransfer(domain)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "slaves-update DOMAIN MASTERIP",
|
76
|
+
"Updates the master IP of a slave domain"
|
77
|
+
def slaves_update(domain, masterip)
|
78
|
+
check_action do
|
79
|
+
client.domains_slaves_updatemasterip(domain, masterip)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "slaves-sync FILE MASTERIP",
|
84
|
+
"Synronises list of slave domains in a file"
|
85
|
+
def slaves_sync(filename, masterip)
|
86
|
+
domains = []
|
87
|
+
File.foreach(filename) do |line|
|
88
|
+
domains << line.strip.split(/\s+/).first
|
89
|
+
end
|
90
|
+
|
91
|
+
check_action do
|
92
|
+
client.domains_slaves_sync(domains, masterip)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
desc "soacheck DOMAIN",
|
97
|
+
"Fetch domain SOA serial number for all nodes"
|
98
|
+
def soacheck(domain)
|
99
|
+
response = client.domains_tools_soacheck(domain)
|
100
|
+
print_table(
|
101
|
+
[['Identifier', 'Type', 'Country', 'SOA', 'Response']] +
|
102
|
+
[['----------', '----', '-------', '---', '--------']] +
|
103
|
+
response[:responses][:masters].map do |node|
|
104
|
+
[node[:ip], "master", '', node[:soa], node[:response]]
|
105
|
+
end +
|
106
|
+
response[:responses][:anycastnodes].map do |node|
|
107
|
+
[node[:ref], 'anycast', node[:country], node[:soa], node[:response]]
|
108
|
+
end
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
desc "version", "Show Esgob Ruby Client version"
|
113
|
+
def version
|
114
|
+
say "Esgob Ruby Client version #{Esgob::VERSION}"
|
115
|
+
end
|
116
|
+
map "--version" => "version"
|
117
|
+
|
118
|
+
private ######################################################################
|
119
|
+
|
120
|
+
def client
|
121
|
+
@client ||= Esgob::Client.new(options[:account], options[:key])
|
122
|
+
end
|
123
|
+
|
124
|
+
def check_action
|
125
|
+
results = yield
|
126
|
+
results = [results] unless results.is_a?(Array)
|
127
|
+
results.each do |result|
|
128
|
+
unless result[:action].nil?
|
129
|
+
say "#{result[:domain]} " + set_color("=> #{result[:action]}", :green, :bold)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
data/lib/esgob/client.rb
CHANGED
@@ -10,19 +10,27 @@ class Esgob::Client
|
|
10
10
|
DEFAULT_API_ENDPOINT = "https://api.esgob.com/1.0/".freeze
|
11
11
|
|
12
12
|
def initialize(*args)
|
13
|
-
if args.first.
|
14
|
-
args.first.each_pair { |k,v| send("#{k}=", v) }
|
13
|
+
if args.first.is_a?(Hash)
|
14
|
+
args.first.each_pair { |k, v| send("#{k}=", v) }
|
15
15
|
else
|
16
16
|
self.account = args[0]
|
17
17
|
self.api_key = args[1]
|
18
18
|
end
|
19
19
|
|
20
|
+
self.account ||= ENV['ESGOB_ACCOUNT']
|
21
|
+
self.api_key ||= ENV['ESGOB_API_KEY']
|
20
22
|
self.endpoint ||= DEFAULT_API_ENDPOINT
|
21
|
-
|
22
|
-
|
23
|
+
|
24
|
+
if account.nil? or account.empty?
|
25
|
+
raise(ArgumentError, "No account name configured for Esgob")
|
26
|
+
end
|
27
|
+
|
28
|
+
if api_key.nil? or api_key.empty?
|
29
|
+
raise(ArgumentError, "No API key configured for Esgob")
|
30
|
+
end
|
23
31
|
end
|
24
32
|
|
25
|
-
def call(function_name, arguments={})
|
33
|
+
def call(function_name, arguments = {})
|
26
34
|
uri = URI(endpoint + function_name)
|
27
35
|
uri.query = build_query(default_arguments.merge(arguments))
|
28
36
|
|
@@ -32,22 +40,28 @@ class Esgob::Client
|
|
32
40
|
http.request(req)
|
33
41
|
end
|
34
42
|
|
35
|
-
if res.
|
36
|
-
|
37
|
-
|
43
|
+
if res.content_type == 'application/json'
|
44
|
+
data = symbolize_keys! JSON.parse(res.body)
|
45
|
+
if data.key?(:error)
|
46
|
+
raise Esgob::ServerError.new(
|
47
|
+
data[:error][:message],
|
48
|
+
data[:error][:code].to_s
|
49
|
+
)
|
50
|
+
elsif res.code !~ /^2/
|
51
|
+
raise Esgob::ServerError.new(res.message, res.code)
|
38
52
|
else
|
39
|
-
|
53
|
+
return data
|
40
54
|
end
|
41
55
|
else
|
42
|
-
|
43
|
-
res.value
|
56
|
+
raise "HTTP response from ESGOB is not of type JSON"
|
44
57
|
end
|
45
|
-
|
46
58
|
end
|
47
59
|
|
48
60
|
# Return account status; credit balance, etc
|
49
61
|
def accounts_get
|
50
|
-
call('accounts.get')
|
62
|
+
account = call('accounts.get')
|
63
|
+
account[:added] = Time.at(account[:added]) if account[:added].is_a?(Fixnum)
|
64
|
+
account
|
51
65
|
end
|
52
66
|
|
53
67
|
# Returns all hosted domains
|
@@ -56,7 +70,7 @@ class Esgob::Client
|
|
56
70
|
end
|
57
71
|
|
58
72
|
# Returns all hosted slave domains as a hash
|
59
|
-
#
|
73
|
+
#
|
60
74
|
def domains_slaves_list
|
61
75
|
Hash[
|
62
76
|
call('domains.slaves.list')[:domains].map do |item|
|
@@ -67,32 +81,44 @@ class Esgob::Client
|
|
67
81
|
|
68
82
|
# Adds a new slave domain
|
69
83
|
def domains_slaves_add(domain, masterip)
|
70
|
-
call('domains.slaves.add', :domain => domain, :masterip => masterip)
|
84
|
+
result = call('domains.slaves.add', :domain => domain, :masterip => masterip)
|
85
|
+
result[:domain] ||= domain
|
86
|
+
result
|
71
87
|
end
|
72
88
|
|
73
89
|
# Deletes a slave domain
|
74
90
|
def domains_slaves_delete(domain)
|
75
|
-
call('domains.slaves.delete', :domain => domain)
|
91
|
+
result = call('domains.slaves.delete', :domain => domain)
|
92
|
+
result[:domain] ||= domain
|
93
|
+
result
|
76
94
|
end
|
77
95
|
|
78
96
|
# Force AXFR / transfer from master of a slave domain
|
79
97
|
def domains_slaves_forcetransfer(domain)
|
80
|
-
call('domains.slaves.forcetransfer', :domain => domain)
|
98
|
+
result = call('domains.slaves.forcetransfer', :domain => domain)
|
99
|
+
result[:domain] ||= domain
|
100
|
+
result
|
81
101
|
end
|
82
102
|
|
83
103
|
# Updates the master IP of a slave domain
|
84
104
|
def domains_slaves_updatemasterip(domain, masterip)
|
85
|
-
call('domains.slaves.updatemasterip', :domain => domain, :masterip => masterip)
|
105
|
+
result = call('domains.slaves.updatemasterip', :domain => domain, :masterip => masterip)
|
106
|
+
result[:domain] ||= domain
|
107
|
+
result
|
86
108
|
end
|
87
109
|
|
88
110
|
# Add a host allowed to AXFR out
|
89
111
|
def domains_slaves_axfrout_add(domain, axfrip)
|
90
|
-
call('domains.slaves.axfrout.add', :domain => domain, :axfrip => axfrip)
|
112
|
+
result = call('domains.slaves.axfrout.add', :domain => domain, :axfrip => axfrip)
|
113
|
+
result[:domain] ||= domain
|
114
|
+
result
|
91
115
|
end
|
92
116
|
|
93
117
|
# Account Delete a host allowed to AXFR out
|
94
118
|
def domains_slaves_axfrout_delete(domain, axfrip)
|
95
|
-
call('domains.slaves.axfrout.delete', :domain => domain, :axfrip => axfrip)
|
119
|
+
result = call('domains.slaves.axfrout.delete', :domain => domain, :axfrip => axfrip)
|
120
|
+
result[:domain] ||= domain
|
121
|
+
result
|
96
122
|
end
|
97
123
|
|
98
124
|
# Retrieve the domain SOA serial number from the master and each anycast node
|
@@ -100,6 +126,44 @@ class Esgob::Client
|
|
100
126
|
call('domains.tools.soacheck', :domain => domain)
|
101
127
|
end
|
102
128
|
|
129
|
+
# Given a list of domains and a master IP, add and delete domains
|
130
|
+
# so that the Esgob account matches the local list
|
131
|
+
def domains_slaves_sync(domains, masterip)
|
132
|
+
existing_domains = domains_slaves_list
|
133
|
+
|
134
|
+
# Add any missing domains
|
135
|
+
responses = []
|
136
|
+
domains.each do |domain|
|
137
|
+
unless existing_domains.include?(domain)
|
138
|
+
response = domains_slaves_add(domain, masterip)
|
139
|
+
response[:domain] ||= domain
|
140
|
+
responses << response
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Now check the existing domains
|
145
|
+
existing_domains.keys.sort.each do |domain|
|
146
|
+
if domains.include?(domain)
|
147
|
+
# Update the masterip if it isn't correct
|
148
|
+
if existing_domains[domain] != masterip
|
149
|
+
response = domains_slaves_updatemasterip(domain, masterip)
|
150
|
+
response[:domain] ||= domain
|
151
|
+
responses << response
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# Delete domain; not on list
|
155
|
+
response = domains_slaves_delete(domain)
|
156
|
+
response[:domain] ||= domain
|
157
|
+
responses << response
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
responses
|
162
|
+
end
|
163
|
+
|
164
|
+
def inspect
|
165
|
+
"\#<#{self.class} account=#{@account}>"
|
166
|
+
end
|
103
167
|
|
104
168
|
protected
|
105
169
|
|
@@ -111,10 +175,10 @@ class Esgob::Client
|
|
111
175
|
when Hash
|
112
176
|
symbolize_keys!(hash[ks])
|
113
177
|
when Array
|
114
|
-
hash[ks].each {|item| symbolize_keys!(item) if item.
|
178
|
+
hash[ks].each { |item| symbolize_keys!(item) if item.is_a?(Hash) }
|
115
179
|
end
|
116
180
|
end
|
117
|
-
|
181
|
+
hash
|
118
182
|
end
|
119
183
|
|
120
184
|
def default_arguments
|
@@ -126,8 +190,8 @@ class Esgob::Client
|
|
126
190
|
end
|
127
191
|
|
128
192
|
def build_query(hash)
|
129
|
-
hash.keys.sort{|a,b| a.to_s <=> b.to_s}.map
|
130
|
-
URI
|
131
|
-
|
193
|
+
hash.keys.sort { |a, b| a.to_s <=> b.to_s }.map do |key|
|
194
|
+
URI.escape(key.to_s) + '=' + URI.escape(hash[key].to_s)
|
195
|
+
end.join('&')
|
132
196
|
end
|
133
197
|
end
|
data/lib/esgob/version.rb
CHANGED
data/lib/esgob.rb
CHANGED
@@ -1,4 +1,14 @@
|
|
1
1
|
module Esgob
|
2
2
|
autoload :Client, 'esgob/client'
|
3
|
-
autoload :
|
3
|
+
autoload :CLI, 'esgob/cli'
|
4
|
+
autoload :VERSION, 'esgob/version'
|
5
|
+
|
6
|
+
class ServerError < StandardError
|
7
|
+
attr_reader :code
|
8
|
+
|
9
|
+
def initialize(message, code = nil)
|
10
|
+
super message
|
11
|
+
@code = code
|
12
|
+
end
|
13
|
+
end
|
4
14
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'esgob'
|
5
|
+
|
6
|
+
class TestCLI < MiniTest::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
# Run before each test
|
9
|
+
FakeWeb.clean_registry
|
10
|
+
|
11
|
+
@client = Esgob::Client.new('acct', 'xxxx')
|
12
|
+
Esgob::Client.stubs(:new).returns(@client)
|
13
|
+
|
14
|
+
ENV['THOR_SHELL'] = 'Basic'
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown
|
18
|
+
# Reset environment variables after each test
|
19
|
+
ENV.delete('ESGOB_ACCOUNT')
|
20
|
+
ENV.delete('ESGOB_API_KEY')
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_account
|
24
|
+
register_fixture('accounts.get')
|
25
|
+
|
26
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(account)) }
|
27
|
+
assert_match " id: xyz\n", output
|
28
|
+
assert_match " name: Person Name\n", output
|
29
|
+
assert_match " credits: 48\n", output
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_account_error
|
33
|
+
FakeWeb.register_uri(
|
34
|
+
:get, %r{^https?://api\.esgob\.com(:443)?/},
|
35
|
+
:status => ['401', 'UNAUTHORIZED'],
|
36
|
+
:content_type => 'application/json',
|
37
|
+
:body => read_fixture(:code_1003)
|
38
|
+
)
|
39
|
+
|
40
|
+
output = capture(:stderr) { Esgob::CLI.start(%w(account)) }
|
41
|
+
assert_equal "=> Error: Account and API key combination not valid [1003]\n", output
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_domains
|
45
|
+
register_fixture('domains.list')
|
46
|
+
|
47
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(domains)) }
|
48
|
+
assert_match "Domain Type\n", output
|
49
|
+
assert_match "------ ----\n", output
|
50
|
+
assert_match "example.com slave\n", output
|
51
|
+
assert_match "example.uk slave\n", output
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_slaves
|
55
|
+
register_fixture('domains.slaves.list')
|
56
|
+
|
57
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(slaves)) }
|
58
|
+
assert_match "Domain Master IP\n", output
|
59
|
+
assert_match "------ ---------\n", output
|
60
|
+
assert_match "example.com 195.177.253.166\n", output
|
61
|
+
assert_match "example.uk 195.177.253.166\n", output
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_slaves_add
|
65
|
+
register_fixture('domains.slaves.add')
|
66
|
+
|
67
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(slaves-add example.org 195.177.253.166)) }
|
68
|
+
assert_equal "example.org => domain added\n", output
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_slaves_delete
|
72
|
+
register_fixture('domains.slaves.delete')
|
73
|
+
|
74
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(slaves-delete example.org)) }
|
75
|
+
assert_equal "example.org => domain deleted\n", output
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_slaves_delete_error
|
79
|
+
FakeWeb.register_uri(
|
80
|
+
:get, %r{^https?://api\.esgob\.com(:443)?/},
|
81
|
+
:status => ['403', 'FORBIDDEN'],
|
82
|
+
:content_type => 'application/json',
|
83
|
+
:body => read_fixture(:code_2007)
|
84
|
+
)
|
85
|
+
|
86
|
+
output = capture(:stderr) { Esgob::CLI.start(%w(slaves-delete example.com)) }
|
87
|
+
assert_equal "=> Error: Domain is not present in your account [2007]\n", output
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_slaves_transfer
|
91
|
+
register_fixture('domains.slaves.forcetransfer')
|
92
|
+
|
93
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(slaves-transfer example.org)) }
|
94
|
+
assert_equal "example.org => Domain AXFR requested from master\n", output
|
95
|
+
end
|
96
|
+
|
97
|
+
def test_slaves_update
|
98
|
+
register_fixture('domains.slaves.updatemasterip')
|
99
|
+
|
100
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(slaves-update example.org 195.177.253.167)) }
|
101
|
+
assert_equal "example.org => domain master IP updated\n", output
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_slaves_sync
|
105
|
+
@client.expects(:domains_slaves_list).with().returns('a.com' => '195.177.253.169', 'b.com' => '195.177.253.169')
|
106
|
+
@client.expects(:domains_slaves_delete).with('a.com').returns(:action => 'domain deleted')
|
107
|
+
@client.expects(:domains_slaves_add).with('c.com', '195.177.253.169').returns(:action => 'domain added')
|
108
|
+
|
109
|
+
output = capture(:stdout) { Esgob::CLI.start(['slaves-sync', fixture_path('sync-domain-list.txt'), '195.177.253.169']) }
|
110
|
+
assert_match "a.com => domain deleted\n", output
|
111
|
+
assert_match "c.com => domain added\n", output
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_soacheck
|
115
|
+
register_fixture('domains.tools.soacheck')
|
116
|
+
|
117
|
+
output = capture(:stdout) { Esgob::CLI.start(%w(soacheck example.org)) }
|
118
|
+
assert_match "Identifier Type Country SOA Response\n", output
|
119
|
+
assert_match "---------- ---- ------- --- --------\n", output
|
120
|
+
assert_match "195.177.253.167 master fail\n", output
|
121
|
+
assert_match "4f31ad80 anycast gb fail\n", output
|
122
|
+
assert_match "fgej72a1 anycast us fail\n", output
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_version
|
126
|
+
assert_match /Esgob Ruby Client version \d+.\d+\.\d+/,
|
127
|
+
capture(:stdout) { Esgob::CLI.start(%w(version)) }
|
128
|
+
end
|
129
|
+
end
|
data/test/esgob_client_test.rb
CHANGED
@@ -44,6 +44,20 @@ class TestClient < MiniTest::Unit::TestCase
|
|
44
44
|
assert_equal 'http://api.example.com/', client.endpoint
|
45
45
|
end
|
46
46
|
|
47
|
+
def test_new_client_with_no_account
|
48
|
+
ENV.delete('ESGOB_ACCOUNT')
|
49
|
+
assert_raises(ArgumentError) do
|
50
|
+
Esgob::Client.new(nil, 'mykey')
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_new_client_with_no_api_key
|
55
|
+
ENV.delete('ESGOB_API_KEY')
|
56
|
+
assert_raises(ArgumentError) do
|
57
|
+
Esgob::Client.new('acct', nil)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
47
61
|
def test_call_with_no_parameters
|
48
62
|
register_fixture('accounts.get')
|
49
63
|
response = @client.call('accounts.get')
|
@@ -53,7 +67,13 @@ class TestClient < MiniTest::Unit::TestCase
|
|
53
67
|
FakeWeb.last_request.path
|
54
68
|
)
|
55
69
|
assert_equal(
|
56
|
-
{
|
70
|
+
{
|
71
|
+
:credits => 48,
|
72
|
+
:users => [],
|
73
|
+
:added => 1422792434,
|
74
|
+
:id => 'xyz',
|
75
|
+
:name => 'Person Name'
|
76
|
+
},
|
57
77
|
response
|
58
78
|
)
|
59
79
|
end
|
@@ -79,27 +99,38 @@ class TestClient < MiniTest::Unit::TestCase
|
|
79
99
|
end
|
80
100
|
|
81
101
|
def test_call_with_404_error
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
102
|
+
FakeWeb.register_uri(
|
103
|
+
:get, %r{^https?://api\.esgob\.com(:443)?/},
|
104
|
+
:status => ['404', 'Not Found'],
|
105
|
+
:content_type => 'application/json',
|
106
|
+
:body => '{}'
|
107
|
+
)
|
108
|
+
err = assert_raises(Esgob::ServerError) { @client.call('accounts.get') }
|
109
|
+
assert_equal 'Not Found', err.message
|
110
|
+
assert_equal '404', err.code
|
91
111
|
end
|
92
112
|
|
93
113
|
def test_call_with_non_json_reponse
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
114
|
+
FakeWeb.register_uri(
|
115
|
+
:get, %r{^https?://api\.esgob\.com(:443)?/},
|
116
|
+
:status => ['200', 'OK'],
|
117
|
+
:content_type => 'text/plain',
|
118
|
+
:body => 'This is plain text'
|
119
|
+
)
|
120
|
+
err = assert_raises(RuntimeError) { @client.call('accounts.get') }
|
121
|
+
assert_equal 'HTTP response from ESGOB is not of type JSON', err.message
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_call_domain_not_present
|
125
|
+
FakeWeb.register_uri(
|
126
|
+
:get, %r{^https?://api\.esgob\.com(:443)?/},
|
127
|
+
:status => ['403', 'FORBIDDEN'],
|
128
|
+
:content_type => 'application/json',
|
129
|
+
:body => read_fixture(:code_2007)
|
130
|
+
)
|
131
|
+
err = assert_raises(Esgob::ServerError) { @client.call('domains.slaves.delete', :domain => 'example.org') }
|
132
|
+
assert_equal 'Domain is not present in your account', err.message
|
133
|
+
assert_equal '2007', err.code
|
103
134
|
end
|
104
135
|
|
105
136
|
def test_accounts_get
|
@@ -111,7 +142,13 @@ class TestClient < MiniTest::Unit::TestCase
|
|
111
142
|
FakeWeb.last_request.path
|
112
143
|
)
|
113
144
|
assert_equal(
|
114
|
-
{
|
145
|
+
{
|
146
|
+
:credits => 48,
|
147
|
+
:users => [],
|
148
|
+
:added => Time.parse('2015-02-01 12:07:14 +0000'),
|
149
|
+
:id => 'xyz',
|
150
|
+
:name => 'Person Name'
|
151
|
+
},
|
115
152
|
response
|
116
153
|
)
|
117
154
|
end
|
@@ -126,8 +163,8 @@ class TestClient < MiniTest::Unit::TestCase
|
|
126
163
|
)
|
127
164
|
assert_equal(
|
128
165
|
[
|
129
|
-
{:domain =>
|
130
|
-
{:domain =>
|
166
|
+
{ :domain => 'example.com', :type => 'slave' },
|
167
|
+
{ :domain => 'example.uk', :type => 'slave' }
|
131
168
|
],
|
132
169
|
response
|
133
170
|
)
|
@@ -142,7 +179,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
142
179
|
FakeWeb.last_request.path
|
143
180
|
)
|
144
181
|
assert_equal(
|
145
|
-
{
|
182
|
+
{ 'example.com' => '195.177.253.166', 'example.uk' => '195.177.253.166' },
|
146
183
|
response
|
147
184
|
)
|
148
185
|
end
|
@@ -155,7 +192,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
155
192
|
'/1.0/domains.slaves.add?account=acct&domain=example.org&f=json&key=xxxx&masterip=195.177.253.166',
|
156
193
|
FakeWeb.last_request.path
|
157
194
|
)
|
158
|
-
assert_equal({:action=>
|
195
|
+
assert_equal({ :action => 'domain added', :domain => 'example.org' }, response)
|
159
196
|
end
|
160
197
|
|
161
198
|
def test_domains_slaves_delete
|
@@ -166,7 +203,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
166
203
|
'/1.0/domains.slaves.delete?account=acct&domain=example.org&f=json&key=xxxx',
|
167
204
|
FakeWeb.last_request.path
|
168
205
|
)
|
169
|
-
assert_equal({:action=>
|
206
|
+
assert_equal({ :action => 'domain deleted', :domain => 'example.org' }, response)
|
170
207
|
end
|
171
208
|
|
172
209
|
def test_domains_slaves_forcetransfer
|
@@ -177,7 +214,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
177
214
|
'/1.0/domains.slaves.forcetransfer?account=acct&domain=example.org&f=json&key=xxxx',
|
178
215
|
FakeWeb.last_request.path
|
179
216
|
)
|
180
|
-
assert_equal({:action=>
|
217
|
+
assert_equal({ :action => 'Domain AXFR requested from master', :domain => 'example.org' }, response)
|
181
218
|
end
|
182
219
|
|
183
220
|
def test_domains_slaves_updatemasterip
|
@@ -188,7 +225,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
188
225
|
'/1.0/domains.slaves.updatemasterip?account=acct&domain=example.org&f=json&key=xxxx&masterip=195.177.253.167',
|
189
226
|
FakeWeb.last_request.path
|
190
227
|
)
|
191
|
-
assert_equal({:action=>
|
228
|
+
assert_equal({ :action => 'domain master IP updated', :domain => 'example.org' }, response)
|
192
229
|
end
|
193
230
|
|
194
231
|
def test_domains_slaves_axfrout_add
|
@@ -199,7 +236,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
199
236
|
'/1.0/domains.slaves.axfrout.add?account=acct&axfrip=195.177.253.1&domain=example.org&f=json&key=xxxx',
|
200
237
|
FakeWeb.last_request.path
|
201
238
|
)
|
202
|
-
assert_equal({:action=>
|
239
|
+
assert_equal({ :action => 'domain AXFR out IPs updated', :domain => 'example.org' }, response)
|
203
240
|
end
|
204
241
|
|
205
242
|
def test_domains_slaves_axfrout_delete
|
@@ -210,7 +247,7 @@ class TestClient < MiniTest::Unit::TestCase
|
|
210
247
|
'/1.0/domains.slaves.axfrout.delete?account=acct&axfrip=195.177.253.1&domain=example.org&f=json&key=xxxx',
|
211
248
|
FakeWeb.last_request.path
|
212
249
|
)
|
213
|
-
assert_equal({:action=>
|
250
|
+
assert_equal({ :action => 'domain AXFR out IPs updated', :domain => 'example.org' }, response)
|
214
251
|
end
|
215
252
|
|
216
253
|
def test_domains_tools_soacheck
|
@@ -225,4 +262,56 @@ class TestClient < MiniTest::Unit::TestCase
|
|
225
262
|
refute_empty(response[:responses])
|
226
263
|
end
|
227
264
|
|
265
|
+
def test_domains_slaves_sync_noop
|
266
|
+
@client.expects(:domains_slaves_list).with().returns(
|
267
|
+
'a.com' => '195.177.253.1', 'b.com' => '195.177.253.1'
|
268
|
+
)
|
269
|
+
|
270
|
+
responses = @client.domains_slaves_sync(['a.com', 'b.com'], '195.177.253.1')
|
271
|
+
assert_equal [], responses
|
272
|
+
end
|
273
|
+
|
274
|
+
def test_domains_slaves_sync_add_only
|
275
|
+
@client.expects(:domains_slaves_list).with().returns({})
|
276
|
+
@client.expects(:domains_slaves_add).with('a.com', '195.177.253.1').returns(:action => 'domain added')
|
277
|
+
@client.expects(:domains_slaves_add).with('b.com', '195.177.253.1').returns(:action => 'domain added')
|
278
|
+
|
279
|
+
responses = @client.domains_slaves_sync(['a.com', 'b.com'], '195.177.253.1')
|
280
|
+
assert_equal [
|
281
|
+
{ :action => 'domain added', :domain => 'a.com' },
|
282
|
+
{ :action => 'domain added', :domain => 'b.com' }
|
283
|
+
], responses
|
284
|
+
end
|
285
|
+
|
286
|
+
def test_domains_slaves_sync_add_and_delete
|
287
|
+
@client.expects(:domains_slaves_list).with().returns({'a.com' => '195.177.253.1'})
|
288
|
+
@client.expects(:domains_slaves_delete).with('a.com').returns({:action => 'domain deleted'})
|
289
|
+
@client.expects(:domains_slaves_add).with('b.com', '195.177.253.1').returns(:action => 'domain added')
|
290
|
+
|
291
|
+
responses = @client.domains_slaves_sync(['b.com'], '195.177.253.1')
|
292
|
+
assert_equal [
|
293
|
+
{ :action => 'domain added', :domain => 'b.com' },
|
294
|
+
{ :action => 'domain deleted', :domain => 'a.com' }
|
295
|
+
], responses
|
296
|
+
end
|
297
|
+
|
298
|
+
def test_domains_slaves_sync_add_and_delete_and_change_masterip
|
299
|
+
@client.expects(:domains_slaves_list).with().returns(
|
300
|
+
'a.com' => '195.177.253.1', 'c.com' => '127.0.0.1'
|
301
|
+
)
|
302
|
+
@client.expects(:domains_slaves_delete).with('a.com').returns(:action => 'domain deleted')
|
303
|
+
@client.expects(:domains_slaves_add).with('b.com', '195.177.253.1').returns(:action => 'domain added')
|
304
|
+
@client.expects(:domains_slaves_updatemasterip).with('c.com', '195.177.253.1').returns(:action => 'domain master IP updated')
|
305
|
+
|
306
|
+
responses = @client.domains_slaves_sync(['b.com', 'c.com'], '195.177.253.1')
|
307
|
+
assert_equal [
|
308
|
+
{ :action => 'domain added', :domain => 'b.com' },
|
309
|
+
{ :action => 'domain deleted', :domain => 'a.com' },
|
310
|
+
{ :action => 'domain master IP updated', :domain => 'c.com' }
|
311
|
+
], responses
|
312
|
+
end
|
313
|
+
|
314
|
+
def test_inspect
|
315
|
+
assert_match('#<Esgob::Client account=acct>', @client.inspect)
|
316
|
+
end
|
228
317
|
end
|
data/test/esgob_version_test.rb
CHANGED
@@ -4,11 +4,9 @@ require 'test_helper'
|
|
4
4
|
require 'esgob'
|
5
5
|
|
6
6
|
class TestVersion < MiniTest::Unit::TestCase
|
7
|
-
|
8
7
|
def test_version_number_looks_sensible
|
9
8
|
assert_equal 'constant', defined?(Esgob::VERSION)
|
10
9
|
assert_kind_of String, Esgob::VERSION
|
11
10
|
assert_match /^\d{1,2}\.\d{1,2}\.\d{1,2}$/, Esgob::VERSION
|
12
11
|
end
|
13
|
-
|
14
12
|
end
|
data/test/test_helper.rb
CHANGED
@@ -4,6 +4,7 @@ require 'rubygems'
|
|
4
4
|
require 'bundler'
|
5
5
|
Bundler.require(:default, :development)
|
6
6
|
require 'minitest/autorun'
|
7
|
+
require 'mocha/mini_test'
|
7
8
|
|
8
9
|
unless RUBY_VERSION =~ /^1\.8/
|
9
10
|
SimpleCov.start
|
@@ -11,18 +12,34 @@ end
|
|
11
12
|
|
12
13
|
FakeWeb.allow_net_connect = false
|
13
14
|
|
15
|
+
def fixture_path(fixture_name)
|
16
|
+
fixture_name = fixture_name.to_s
|
17
|
+
fixture_name += '.json' unless fixture_name.match(/\.\w+$/)
|
18
|
+
File.join(File.dirname(__FILE__), 'fixtures', fixture_name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_fixture(fixture_name)
|
22
|
+
File.read fixture_path(fixture_name)
|
23
|
+
end
|
14
24
|
|
15
|
-
def register_fixture(api_call, fixture_name=nil)
|
25
|
+
def register_fixture(api_call, fixture_name = nil)
|
16
26
|
if fixture_name.nil?
|
17
27
|
fixture_name = api_call.gsub(/\W+/, '_')
|
18
28
|
end
|
19
29
|
|
20
|
-
fixture_file = File.join(File.dirname(__FILE__), 'fixtures', fixture_name + '.json')
|
21
|
-
|
22
30
|
FakeWeb.register_uri(
|
23
31
|
:get, %r[^https?://api\.esgob\.com(:443)?/1.0/#{api_call}],
|
24
32
|
:status => ["200", "OK"],
|
25
33
|
:content_type => "application/json",
|
26
|
-
:body =>
|
34
|
+
:body => read_fixture(fixture_name)
|
27
35
|
)
|
28
36
|
end
|
37
|
+
|
38
|
+
def capture(stream)
|
39
|
+
original = eval "$#{stream}"
|
40
|
+
eval "$#{stream} = StringIO.new"
|
41
|
+
yield
|
42
|
+
result = eval("$#{stream}").string
|
43
|
+
eval "$#{stream} = original"
|
44
|
+
result
|
45
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: esgob
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nicholas Humfrey
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: json
|
@@ -24,6 +24,20 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.8'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.19.1
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.19.1
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: bundler
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: 1.3.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: mocha
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 1.1.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 1.1.0
|
83
111
|
- !ruby/object:Gem::Dependency
|
84
112
|
name: simplecov
|
85
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -97,7 +125,8 @@ dependencies:
|
|
97
125
|
description:
|
98
126
|
email:
|
99
127
|
- njh@aelius.com
|
100
|
-
executables:
|
128
|
+
executables:
|
129
|
+
- esgob
|
101
130
|
extensions: []
|
102
131
|
extra_rdoc_files: []
|
103
132
|
files:
|
@@ -107,13 +136,18 @@ files:
|
|
107
136
|
- LICENSE.md
|
108
137
|
- README.md
|
109
138
|
- Rakefile
|
139
|
+
- bin/esgob
|
110
140
|
- esgob.gemspec
|
111
141
|
- lib/esgob.rb
|
142
|
+
- lib/esgob/cli.rb
|
112
143
|
- lib/esgob/client.rb
|
113
144
|
- lib/esgob/version.rb
|
145
|
+
- test/esgob_cli_test.rb
|
114
146
|
- test/esgob_client_test.rb
|
115
147
|
- test/esgob_version_test.rb
|
116
148
|
- test/fixtures/accounts_get.json
|
149
|
+
- test/fixtures/code_1003.json
|
150
|
+
- test/fixtures/code_2007.json
|
117
151
|
- test/fixtures/domains_list.json
|
118
152
|
- test/fixtures/domains_slaves_add.json
|
119
153
|
- test/fixtures/domains_slaves_axfrout_add.json
|
@@ -123,8 +157,9 @@ files:
|
|
123
157
|
- test/fixtures/domains_slaves_list.json
|
124
158
|
- test/fixtures/domains_slaves_updatemasterip.json
|
125
159
|
- test/fixtures/domains_tools_soacheck.json
|
160
|
+
- test/fixtures/sync-domain-list.txt
|
126
161
|
- test/test_helper.rb
|
127
|
-
homepage:
|
162
|
+
homepage: http://github.com/njh/ruby-esgob
|
128
163
|
licenses:
|
129
164
|
- MIT
|
130
165
|
metadata: {}
|
@@ -149,9 +184,12 @@ signing_key:
|
|
149
184
|
specification_version: 4
|
150
185
|
summary: Client library for talking to the Esgob anycast DNS API.
|
151
186
|
test_files:
|
187
|
+
- test/esgob_cli_test.rb
|
152
188
|
- test/esgob_client_test.rb
|
153
189
|
- test/esgob_version_test.rb
|
154
190
|
- test/fixtures/accounts_get.json
|
191
|
+
- test/fixtures/code_1003.json
|
192
|
+
- test/fixtures/code_2007.json
|
155
193
|
- test/fixtures/domains_list.json
|
156
194
|
- test/fixtures/domains_slaves_add.json
|
157
195
|
- test/fixtures/domains_slaves_axfrout_add.json
|
@@ -161,5 +199,6 @@ test_files:
|
|
161
199
|
- test/fixtures/domains_slaves_list.json
|
162
200
|
- test/fixtures/domains_slaves_updatemasterip.json
|
163
201
|
- test/fixtures/domains_tools_soacheck.json
|
202
|
+
- test/fixtures/sync-domain-list.txt
|
164
203
|
- test/test_helper.rb
|
165
204
|
has_rdoc:
|