devdnsd 1.0.0

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,94 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module DevDNSd
8
+ # This class holds the configuration of the applicaton.
9
+ class Configuration
10
+ # If to run the server in foreground. Default: `false`.
11
+ attr_accessor :foreground
12
+
13
+ # The address to listen to. Default: `0.0.0.0`.
14
+ attr_accessor :address
15
+
16
+ # The port to listen to. Default: `7771`.
17
+ attr_accessor :port
18
+
19
+ # The TLD to manage. Default: `dev`.
20
+ attr_accessor :tld
21
+
22
+ # The file to log to. Default: `/var/log/devdnsd.log`.
23
+ attr_accessor :log_file
24
+
25
+ # The minimum severity to log. Default: `Logger::INFO`.
26
+ attr_accessor :log_level
27
+
28
+ # The rules of the server. By default, every hostname is resolved with `127.0.0.1`.
29
+ attr_accessor :rules
30
+
31
+ # Creates a new configuration.
32
+ # A configuration file is a plain Ruby file with a top-level {Configuration config} object.
33
+ #
34
+ # Example:
35
+ #
36
+ # ```ruby
37
+ # config.add_rule("match.dev", "10.0.0.1")
38
+ # ```
39
+ #
40
+ # @param file [String] The file to read.
41
+ # @param application [Application] The application which this configuration is attached to.
42
+ # @param overrides [Hash] A set of values which override those set in the configuration file.
43
+ def initialize(file = nil, application = nil, overrides = {})
44
+ @address = "0.0.0.0"
45
+ @port = 7771
46
+ @tld = "dev"
47
+ @log_file = "/var/log/devdnsd.log"
48
+ @log_level = Logger::INFO
49
+ @rules = []
50
+ @foreground = false
51
+
52
+ if file.present?
53
+ begin
54
+ # Open the file
55
+ path = Pathname.new(file).realpath
56
+ application.logger.info("Using configuration file #{path}.") if application
57
+ self.tap do |config|
58
+ eval(File.read(path))
59
+ end
60
+
61
+ @log_file = $stdout if @log_file == "STDOUT"
62
+ @log_file = $stderr if @log_file == "STDERR"
63
+ rescue Errno::ENOENT, LoadError
64
+ rescue Exception => e
65
+ raise DevDNSd::Errors::InvalidConfiguration.new("Config file #{file} is not valid.")
66
+ end
67
+ end
68
+
69
+ # Apply overrides
70
+ if overrides.is_a?(Hash) then
71
+ overrides.each_pair do |k, v|
72
+ self.send("#{k}=", v) if self.respond_to?("#{k}=") && !v.nil?
73
+ end
74
+ end
75
+
76
+ # Make sure some arguments are of correct type
77
+ @port = @port.to_integer
78
+ @log_level = @log_level.to_integer
79
+
80
+ # Add a default rule
81
+ self.add_rule(/.+/, "127.0.0.1") if @rules.length == 0
82
+ end
83
+
84
+ # Adds a rule to the configuration.
85
+ #
86
+ # @param args [Array] The rule's arguments.
87
+ # @param block [Proc] An optional block for the rule.
88
+ # @return [Array] The current set of rule.
89
+ # @see Rule.create
90
+ def add_rule(*args, &block)
91
+ @rules << DevDNSd::Rule.create(*args, &block)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,18 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module DevDNSd
8
+ # Exceptions for {DevDNSd DevDNSd}.
9
+ module Errors
10
+ # This exception is raised if a {Rule Rule} is invalid.
11
+ class InvalidRule < ArgumentError
12
+ end
13
+
14
+ # This exception is raised if a {Configuration Configuration} is invalid.
15
+ class InvalidConfiguration < ArgumentError
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,84 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdnsd gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module DevDNSd
8
+ # A custom logger for DevDNSd.
9
+ class Logger < ::Logger
10
+ # The start time of first line. This allows to show a `T+0.1234` information into the log.
11
+ mattr_accessor :start_time
12
+
13
+ # The file or device to log messages to.
14
+ attr_reader :device
15
+
16
+ # Creates a new logger
17
+ # @see http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html
18
+ #
19
+ # @param logdev [String|IO] The log device. This is a filename (String) or IO object (typically STDOUT, STDERR, or an open file).
20
+ # @param shift_age [Fixnum] Number of old log files to keep, or frequency of rotation (daily, weekly or monthly).
21
+ # @param shift_size [Fixnum] Maximum logfile size (only applies when shift_age is a number).
22
+ def initialize(logdev, shift_age = 0, shift_size = 1048576)
23
+ @device = logdev
24
+ super(logdev, shift_age, shift_size)
25
+ end
26
+
27
+ # Creates a new logger
28
+ #
29
+ # @param file [String|IO] The log device. This is a filename (String) or IO object (typically STDOUT, STDERR, or an open file).
30
+ # @param level [Fixnum] The minimum severity to log. See http://www.ruby-doc.org/stdlib-1.9.3/libdoc/logger/rdoc/Logger.html for valid levels.
31
+ # @param formatter [Proc] The formatter to use for logging.
32
+ # @return [Logger] The new logger.
33
+ def self.create(file = nil, level = Logger::INFO, formatter = nil)
34
+ file ||= self.default_file
35
+ rv = self.new(self.get_real_file(file))
36
+ rv.level = level.to_i
37
+ rv.formatter = formatter || self.default_formatter
38
+ rv
39
+ end
40
+
41
+ # Translates a file to standard input or standard ouput in some special cases.
42
+ #
43
+ # @param file [String] The string to translate.
44
+ # @return [String|IO] The translated file name.
45
+ def self.get_real_file(file)
46
+ case file
47
+ when "STDOUT" then $stdout
48
+ when "STDERR" then $stderr
49
+ else file
50
+ end
51
+ end
52
+
53
+ # The default file for logging.
54
+ # @return [String|IO] The default file for logging.
55
+ def self.default_file
56
+ @default_file ||= $stdout
57
+ end
58
+
59
+ # The default formatter for logging.
60
+ # @return [Proc] The default formatter for logging.
61
+ def self.default_formatter
62
+ @default_formatter ||= Proc.new {|severity, datetime, progname, msg|
63
+ color = case severity
64
+ when "DEBUG" then :cyan
65
+ when "INFO" then :green
66
+ when "WARN" then :yellow
67
+ when "ERROR" then :red
68
+ when "FATAL" then :magenta
69
+ else :white
70
+ end
71
+
72
+ header = ("[%s T+%0.5f] %s:" %[datetime.strftime("%Y/%b/%d %H:%M:%S"), [datetime.to_f - self.start_time.to_f, 0].max, severity.rjust(5)]).bright
73
+ header = header.color(color) if color.present?
74
+ "%s %s\n" % [header, msg]
75
+ }
76
+ end
77
+
78
+ # The log time of the first logger. This allows to show a `T+0.1234` information into the log.
79
+ # @return [Time] The log time of the first logger.
80
+ def self.start_time
81
+ @start_time ||= Time.now
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,130 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module DevDNSd
8
+ # This class encapsulate a rule for matching an hostname.
9
+ class Rule
10
+ # The pattern to match. Default: `/.+/`.
11
+ attr_accessor :match
12
+
13
+ # The type of request to match. Default: `:A`.
14
+ #
15
+ # @see .create
16
+ attr_accessor :type
17
+
18
+ # The IP or hostname to reply back to the client. Default: `127.0.0.1`.
19
+ #
20
+ # @see .create
21
+ attr_accessor :reply
22
+
23
+ # A list of options for the request. Default is an empty hash.
24
+ attr_accessor :options
25
+
26
+ # An optional block to compute the reply instead of using the `reply` parameter.
27
+ #
28
+ # @see .create
29
+ attr_accessor :block
30
+
31
+ # Creates a new rule.
32
+ #
33
+ # @param match [String|Regexp] The pattern to match.
34
+ # @param reply [String] The IP or hostname to reply back to the client.
35
+ # @param type [Symbol] The type of request to match.
36
+ # @param options [Hash] A list of options for the request.
37
+ # @param block [Proc] An optional block to compute the reply instead of using the `reply` parameter.
38
+ # @see .create
39
+ def initialize(match = /.+/, reply = "127.0.0.1", type = :A, options = {}, &block)
40
+ reply ||= "127.0.0.1"
41
+ type ||= :A
42
+ @match = match
43
+ @type = type
44
+ @reply = block.blank? ? reply : nil
45
+ @options = options
46
+ @block = block
47
+
48
+ raise(DevDNSd::Errors::InvalidRule.new("You must specify at least a rule and a host (also via a block). Optionally you can add a record type (default: A) and the options.")) if @reply.blank? && @block.nil?
49
+ raise(DevDNSd::Errors::InvalidRule.new("You can only use hashs for options.")) if !@options.is_a?(Hash)
50
+ end
51
+
52
+ # Returns the resource class(es) for the current rule.
53
+ #
54
+ # @return [Array|Class] The class(es) for the current rule.
55
+ def resource_class
56
+ classes = self.type.ensure_array.collect {|cls| self.class.symbol_to_resource_class(cls) }.compact.uniq
57
+ classes.length == 1 ? classes.first : classes
58
+ end
59
+
60
+ # Checks if the rule is a regexp.
61
+ #
62
+ # @return [Boolean] `true` if the rule is a Regexp, `false` otherwise.
63
+ def is_regexp?
64
+ self.match.is_a?(Regexp)
65
+ end
66
+
67
+ # Checks if the rule is a regexp.
68
+ #
69
+ # @return [Boolean] `true` if the rule has a block, `false` otherwise.
70
+ def has_block?
71
+ self.block.present?
72
+ end
73
+
74
+ # Matches a hostname to the rule.
75
+ #
76
+ # @param [String] The hostname to match.
77
+ # @return [MatchData|Boolean|Nil] Return `true` or MatchData (if the pattern is a regexp) if the rule matches, `false` or `nil` otherwise.
78
+ def match_host(hostname)
79
+ self.is_regexp? ? self.match.match(hostname) : (self.match == hostname)
80
+ end
81
+
82
+ # Creates a new rule.
83
+ #
84
+ # @param match [String|Regexp] The pattern to match.
85
+ # @param reply_or_type [String|Symbol] The IP or hostname to reply back to the client (or the type of request to match, if a block is provided).
86
+ # @param type [Symbol] The type of request to match. This is ignored if a block is provided.
87
+ # @param options [Hash] A list of options for the request.
88
+ # @param block [Proc] An optional block to compute the reply instead of using the `reply_or_type` parameter. In this case `reply_or_type` is used for the type of the request and `type` is ignored.
89
+ def self.create(match, reply_or_type = nil, type = nil, options = {}, &block)
90
+ raise(DevDNSd::Errors::InvalidRule.new("You must specify at least a rule and a host (also via a block). Optionally you can add a record type (default: A) and the options.")) if reply_or_type.blank? && block.nil?
91
+ raise(DevDNSd::Errors::InvalidRule.new("You can only use hashs for options.")) if !options.is_a?(Hash)
92
+
93
+ rv = self.new(match)
94
+ rv.options = options
95
+
96
+ if block.present? then # reply_or_type acts like a type, type is ignored
97
+ rv.type = reply_or_type || :A
98
+ rv.reply = nil
99
+ rv.block = block
100
+ else # reply_or_type acts like a reply
101
+ rv.reply = reply_or_type || "127.0.0.1"
102
+ rv.type = type || :A
103
+ end
104
+
105
+ rv
106
+ end
107
+
108
+ # Converts a class to the correspondent symbol.
109
+ #
110
+ # @param klass [Class] The class to convert.
111
+ # @return [Symbol] The symbol representation of the class.
112
+ def self.resource_class_to_symbol(klass)
113
+ klass.to_s.gsub(/(.+::)?(.+)/, "\\2").to_sym
114
+ end
115
+
116
+ # Converts a symbol to the correspondent DNS resource class.
117
+ #
118
+ # @param symbol [Symbol] The symbol to convert.
119
+ # @return [Symbol] The class associated to the symbol.
120
+ def self.symbol_to_resource_class(symbol)
121
+ symbol = symbol.to_s.upcase
122
+
123
+ begin
124
+ "Resolv::DNS::Resource::IN::#{symbol}".constantize
125
+ rescue NameError
126
+ raise(DevDNSd::Errors::InvalidRule.new("Invalid resource class #{symbol}."))
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ module DevDNSd
8
+ # The current version of DevDNSd.
9
+ # For version, semantic version is used. See: http://semver.org/
10
+ module Version
11
+ # The major version.
12
+ MAJOR = 1
13
+
14
+ # The minor version.
15
+ MINOR = 0
16
+
17
+ # The patch version.
18
+ PATCH = 0
19
+
20
+ # The current version number of DevDNSd.
21
+ STRING = [MAJOR, MINOR, PATCH].compact.join(".")
22
+ end
23
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ require "simplecov"
8
+ require "pathname"
9
+
10
+ if ENV["DEVDNSD_COVERAGE"] == "TRUE" then
11
+ root = Pathname.new(File.dirname(__FILE__)) + ".."
12
+
13
+ SimpleCov.start do
14
+ add_filter do |src_file|
15
+ path = Pathname.new(src_file.filename).relative_path_from(root).to_s
16
+ path =~ /^lib\/devdnsd\/patches/ || path !~ /^(bin|lib)/
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,436 @@
1
+ # encoding: utf-8
2
+ #
3
+ # This file is part of the devdns gem. Copyright (C) 2012 and above Shogun <shogun_panda@me.com>.
4
+ # Licensed under the MIT license, which can be found at http://www.opensource.org/licenses/mit-license.php.
5
+ #
6
+
7
+ require "spec_helper"
8
+
9
+ describe DevDNSd::Application do
10
+ before(:each) do
11
+ DevDNSd::Logger.stub(:default_file).and_return("/dev/null")
12
+ end
13
+
14
+ let(:application){ app = DevDNSd::Application.instance }
15
+ let(:executable) { Pathname.new(File.dirname((__FILE__))) + "../../bin/devdnsd" }
16
+ let(:sample_config) { Pathname.new(File.dirname((__FILE__))) + "../../config/devdnsd_config.sample" }
17
+ let(:resolver_path) { "/tmp/devdnsd-test-resolver-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}" }
18
+ let(:launch_agent_path) { "/tmp/devdnsd-test-agent-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}" }
19
+
20
+ describe "#initialize" do
21
+ it("should setup the logger") do application.logger.should_not be_nil end
22
+ it("should setup the configuration") do application.config.should_not be_nil end
23
+
24
+ it("should abort with an invalid configuration") do
25
+ path = "/tmp/devdnsd-test-#{Time.now.strftime("%Y%m%d-%H:%M:%S")}"
26
+ file = File.new(path, "w")
27
+ file.write("config.port = ")
28
+ file.close
29
+
30
+ expect { DevDNSd::Application.new({:config => file.path}) }.to raise_error(SystemExit)
31
+ File.unlink(path)
32
+ end
33
+ end
34
+
35
+ describe "#run" do
36
+ it "should run the server" do
37
+ application.should_receive(:perform_server)
38
+
39
+ Thread.start {
40
+ devdnsd_resolv("match.dev")
41
+ DevDNSd::Application.quit
42
+ }
43
+
44
+ DevDNSd::Application.run
45
+ end
46
+ end
47
+
48
+ describe "#perform_server" do
49
+ before(:each) do
50
+ DevDNSd::Logger.stub(:default_file).and_return($stdout)
51
+ end
52
+
53
+ it "should run the server" do
54
+ class DevDNSd::Application
55
+ def on_start
56
+ EventMachine.add_periodic_timer(1) do
57
+ devdnsd_resolv("match.dev")
58
+ DevDNSd::Application.quit
59
+ end
60
+ end
61
+ end
62
+
63
+ RubyDNS.should_receive(:run_server)
64
+ DevDNSd::Application.instance.perform_server
65
+ end
66
+
67
+ it "should iterate the rules" do
68
+ class DevDNSd::Application
69
+ def on_start
70
+ EventMachine.add_periodic_timer(1) do
71
+ devdnsd_resolv("match.dev")
72
+ DevDNSd::Application.quit
73
+ end
74
+ end
75
+ end
76
+
77
+ DevDNSd::Application.instance.config.rules.should_receive(:each).at_least(1)
78
+ DevDNSd::Application.instance.perform_server
79
+ end
80
+
81
+ it "should call process_rule" do
82
+ class DevDNSd::Application
83
+ def on_start
84
+ EventMachine.add_periodic_timer(1) do
85
+ devdnsd_resolv("match.dev")
86
+ DevDNSd::Application.quit
87
+ end
88
+ end
89
+ end
90
+
91
+ DevDNSd::Application.instance.should_receive(:process_rule).at_least(1)
92
+ DevDNSd::Application.instance.perform_server
93
+ end
94
+
95
+ it "should complain about wrong rules" do
96
+ class DevDNSd::Application
97
+ def on_start
98
+ EventMachine.add_periodic_timer(1) do
99
+ devdnsd_resolv("invalid.dev")
100
+ DevDNSd::Application.quit
101
+ end
102
+ end
103
+ end
104
+
105
+ DevDNSd::Application.instance.stub(:process_rule).and_raise(Exception)
106
+ expect { DevDNSd::Application.instance.perform_server }.to raise_exception
107
+ end
108
+
109
+ describe "should correctly resolve hostnames" do
110
+ before(:all) do
111
+ system("ruby \"#{executable}\" -L 0 -l /dev/null -c \"#{sample_config}\" start > /dev/null 2>&1")
112
+ end
113
+
114
+ after(:all) do
115
+ system("ruby \"#{executable}\" -L 0 -l /dev/null stop > /dev/null 2>&1")
116
+ end
117
+
118
+ it "basing on a exact pattern" do
119
+ devdnsd_resolv("match_1.dev").should == ["10.0.1.1", :A]
120
+ devdnsd_resolv("match_2.dev").should == ["10.0.2.1", :MX]
121
+ devdnsd_resolv("match_3.dev").should == ["10.0.3.1", :A]
122
+ devdnsd_resolv("match_4.dev").should == ["10.0.4.1", :CNAME]
123
+ end
124
+
125
+ it "basing on a regexp pattern" do
126
+ devdnsd_resolv("match_5_11.dev").should == ["10.0.5.11", :A]
127
+ devdnsd_resolv("match_5_22.dev").should == ["10.0.5.22", :A]
128
+ devdnsd_resolv("match_6_33.dev").should == ["10.0.6.33", :PTR]
129
+ devdnsd_resolv("match_6_44.dev").should == ["10.0.6.44", :PTR]
130
+ devdnsd_resolv("match_7_55.dev").should == ["10.0.7.55", :A]
131
+ devdnsd_resolv("match_7_66.dev").should == ["10.0.7.66", :A]
132
+ devdnsd_resolv("match_8_77.dev").should == ["10.0.8.77", :PTR]
133
+ devdnsd_resolv("match_8_88.dev").should == ["10.0.8.88", :PTR]
134
+
135
+ end
136
+
137
+ it "and return multiple answsers" do devdnsd_resolv("match_10.dev").should == [["10.0.10.1", :A], ["10.0.10.2", :MX]] end
138
+
139
+ it "and reject invalid matches (with or without rules)" do
140
+ devdnsd_resolv("match_9.dev").should be_nil
141
+ devdnsd_resolv("invalid.dev").should be_nil
142
+ end
143
+ end
144
+ end
145
+
146
+ describe "#process_rule" do
147
+ class FakeTransaction
148
+ attr_reader :resource_class
149
+
150
+ def initialize(cls = Resolv::DNS::Resource::IN::ANY)
151
+ @resource_class = cls
152
+ end
153
+
154
+ def respond!(*args)
155
+ true
156
+ end
157
+ end
158
+
159
+ let(:application){ DevDNSd::Application.new({:config => Pathname.new(File.dirname((__FILE__))) + "../../config/devdnsd_config.sample" }) }
160
+ let(:transaction){ FakeTransaction.new }
161
+
162
+ it "should match a valid string request" do
163
+ rule = application.config.rules[0]
164
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
165
+ end
166
+
167
+ it "should match a valid string request with specific type" do
168
+ rule = application.config.rules[1]
169
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
170
+ end
171
+
172
+ it "should match a valid string request with a block" do
173
+ rule = application.config.rules[2]
174
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
175
+ end
176
+
177
+ it "should match a valid string request with a block" do
178
+ rule = application.config.rules[3]
179
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_true
180
+ end
181
+
182
+
183
+ it "should match a valid regexp request" do
184
+ rule = application.config.rules[4]
185
+ mo = rule.match_host("match_5_12.dev")
186
+ application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
187
+ end
188
+
189
+ it "should match a valid regexp request with specific type" do
190
+ rule = application.config.rules[5]
191
+ mo = rule.match_host("match_6_34.dev")
192
+ application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
193
+ end
194
+
195
+ it "should match a valid regexp request with a block" do
196
+ rule = application.config.rules[6]
197
+ mo = rule.match_host("match_7_56.dev")
198
+ application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
199
+ end
200
+
201
+ it "should match a valid regexp request with a block and specific type" do
202
+ rule = application.config.rules[7]
203
+ mo = rule.match_host("match_8_78.dev")
204
+ application.process_rule(rule, rule.resource_class, mo, transaction).should be_true
205
+ end
206
+
207
+ it "should return false for a false block" do
208
+ rule = application.config.rules[8]
209
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_false
210
+ end
211
+
212
+ it "should return nil for a nil reply" do
213
+ rule = application.config.rules[0]
214
+ rule.reply = nil
215
+ application.process_rule(rule, rule.resource_class, nil, transaction).should be_nil
216
+ end
217
+ end
218
+
219
+ describe "#dns_update" do
220
+ it "should update the DNS cache" do
221
+ application.stub(:execute_command).and_return("EXECUTED")
222
+ application.dns_update.should == "EXECUTED"
223
+ end
224
+ end
225
+
226
+ describe "#resolver_path" do
227
+ it "should return the resolver file basing on the configuration" do application.resolver_path.should == "/etc/resolver/#{application.config.tld}" end
228
+ it "should return the resolver file basing on the argument" do application.resolver_path("foo").should == "/etc/resolver/foo" end
229
+ end
230
+
231
+ describe "launch_agent_path" do
232
+ it "should return the agent file with a default name" do application.launch_agent_path.should == ENV["HOME"] + "/Library/LaunchAgents/it.cowtech.devdnsd.plist" end
233
+ it "should return the agent file with a specified name" do application.launch_agent_path("foo").should == ENV["HOME"] + "/Library/LaunchAgents/foo.plist" end
234
+ end
235
+
236
+ describe "#action_start" do
237
+ it "should call perform_server in foreground" do
238
+ DevDNSd::Application.instance({}, {:foreground => true}, [], true).should_receive(:perform_server)
239
+ application.action_start
240
+ end
241
+
242
+ it "should start the daemon" do
243
+ DevDNSd::Application.instance({}, {}, [], true)
244
+ RExec::Daemon::Controller.should_receive(:start)
245
+ application.action_start
246
+ end
247
+ end
248
+
249
+ describe "#action_stop" do
250
+ it "should stop the daemon" do
251
+ RExec::Daemon::Controller.should_receive(:stop)
252
+ application.action_stop
253
+ end
254
+ end
255
+
256
+ describe "#action_install" do
257
+ it "should create the resolver" do
258
+ application.stub(:resolver_path).and_return(resolver_path)
259
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
260
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
261
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
262
+
263
+ application.action_install
264
+ File.exists?(resolver_path).should be_true
265
+
266
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
267
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
268
+ end
269
+
270
+ it "should create the agent" do
271
+ application.stub(:resolver_path).and_return(resolver_path)
272
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
273
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
274
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
275
+
276
+ application.stub(:resolver_path).and_return(resolver_path)
277
+ application.action_install
278
+ File.exists?(application.launch_agent_path).should be_true
279
+
280
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
281
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
282
+ end
283
+
284
+ it "should update the DNS cache" do
285
+ application.stub(:resolver_path).and_return(resolver_path)
286
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
287
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
288
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
289
+
290
+ application.should_receive(:dns_update)
291
+ application.action_install
292
+
293
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
294
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
295
+ end
296
+
297
+ it "should not create and invalid logger" do
298
+ application.stub(:resolver_path).and_return("/invalid/resolver")
299
+ application.stub(:launch_agent_path).and_return("/invalid/agent")
300
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
301
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
302
+
303
+ application.get_logger.should_receive(:error).with("Cannot create the resolver file.")
304
+ application.action_install
305
+
306
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
307
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
308
+ end
309
+
310
+ it "should not create and invalid agent" do
311
+ application.stub(:resolver_path).and_return(resolver_path)
312
+ application.stub(:launch_agent_path).and_return("/invalid/agent")
313
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
314
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
315
+
316
+ application.get_logger.should_receive(:error).with("Cannot create the launch agent.")
317
+ application.action_install
318
+
319
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
320
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
321
+ end
322
+
323
+ it "should not load an invalid agent" do
324
+ class DevDNSd::Application
325
+ def execute_command(command)
326
+ command =~ /^launchctl/ ? raise(StandardError) : system(command)
327
+ end
328
+ end
329
+
330
+ application.stub(:resolver_path).and_return(resolver_path)
331
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
332
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
333
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
334
+
335
+ application.get_logger.should_receive(:error).with("Cannot load the launch agent.")
336
+ application.action_install
337
+
338
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
339
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
340
+ end
341
+
342
+ it "should raise an exception if not running on OSX" do
343
+ application.stub(:is_osx?).and_return(false)
344
+ application.get_logger.should_receive(:fatal).with("Install DevDNSd as a local resolver is only available on MacOSX.")
345
+ application.action_install.should be_false
346
+ end
347
+ end
348
+
349
+ describe "#action_uninstall" do
350
+ it "should remove the resolver" do
351
+ application.stub(:resolver_path).and_return(resolver_path)
352
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
353
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
354
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
355
+
356
+ application.action_install
357
+ application.action_uninstall
358
+ File.exists?(resolver_path).should be_false
359
+
360
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
361
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
362
+ end
363
+
364
+ it "should remove the agent" do
365
+ application.stub(:resolver_path).and_return(resolver_path)
366
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
367
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
368
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
369
+
370
+ DevDNSd::Logger.stub(:default_file).and_return($stdout)
371
+ application.action_install
372
+ application.action_uninstall
373
+ File.exists?(application.launch_agent_path).should be_false
374
+
375
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
376
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
377
+ end
378
+
379
+ it "should not load delete an invalid resolver" do
380
+ application.stub(:resolver_path).and_return("/invalid/resolver")
381
+ application.stub(:launch_agent_path).and_return("/invalid/agent")
382
+
383
+ application.action_install
384
+ application.get_logger.should_receive(:warn).at_least(1)
385
+ application.action_uninstall
386
+
387
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
388
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
389
+ end
390
+
391
+ it "should not delete an invalid agent" do
392
+ application.stub(:resolver_path).and_return(resolver_path)
393
+ application.stub(:launch_agent_path).and_return("/invalid/agent")
394
+
395
+ application.action_install
396
+ application.get_logger.should_receive(:warn).at_least(1)
397
+ application.action_uninstall
398
+
399
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
400
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
401
+ end
402
+
403
+ it "should not load delete invalid agent" do
404
+ application.stub(:resolver_path).and_return(resolver_path)
405
+ application.stub(:launch_agent_path).and_return("/invalid/agent")
406
+
407
+ application.action_install
408
+ application.stub(:execute_command).and_raise(StandardError)
409
+ application.get_logger.should_receive(:warn).at_least(1)
410
+ application.action_uninstall
411
+
412
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
413
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
414
+ end
415
+
416
+ it "should update the DNS cache" do
417
+ application.stub(:resolver_path).and_return(resolver_path)
418
+ application.stub(:launch_agent_path).and_return(launch_agent_path)
419
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
420
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
421
+
422
+ application.action_install
423
+ application.should_receive(:dns_update)
424
+ application.action_uninstall
425
+
426
+ File.unlink(application.resolver_path) if File.exists?(application.resolver_path)
427
+ File.unlink(application.launch_agent_path) if File.exists?(application.launch_agent_path)
428
+ end
429
+
430
+ it "should raise an exception if not running on OSX" do
431
+ application.stub(:is_osx?).and_return(false)
432
+ application.get_logger.should_receive(:fatal).with("Install DevDNSd as a local resolver is only available on MacOSX.")
433
+ application.action_uninstall.should be_false
434
+ end
435
+ end
436
+ end