devdnsd 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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