hermod 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ require 'xml'
2
+ require 'hermod/xml_section_builder'
3
+ require 'hermod/sanitisation'
4
+
5
+ module Hermod
6
+ # A representation of a section of XML sent to HMRC using the Government
7
+ # Gateway
8
+ class XmlSection
9
+ include Sanitisation
10
+
11
+ # Public: builds a new class using the XmlSectionBuilder DSL
12
+ #
13
+ # Returns the new Class
14
+ def self.build(options = {}, &block)
15
+ Class.new(XmlSection).tap do |new_class|
16
+ options.each do |name, value|
17
+ new_class.public_send "#{name}=", value
18
+ end
19
+ XmlSectionBuilder.new(new_class).build(&block)
20
+ end
21
+ end
22
+
23
+ attr_reader :attributes
24
+
25
+ # Public: turns the XmlSection into an XML::Node instance (from
26
+ # libxml-ruby). This creates this as a node, adds any attributes (after
27
+ # sanitising them according to HMRC's rules) and then adds child nodes in
28
+ # the order they were defined in the DSL. Nodes that have been called multiple
29
+ # times are added in the order they were called.
30
+ #
31
+ # Returns an XML::Node
32
+ def to_xml
33
+ XML::Node.new(self.class.xml_name).tap do |root_node|
34
+ # Add attributes
35
+ attributes.each do |attribute_name, attribute_value|
36
+ sane_value = sanitise_attribute(attribute_value)
37
+ root_node[attribute_name] = sane_value if sane_value.present?
38
+ end
39
+ # Add child nodes
40
+ self.class.node_order.each do |node_name|
41
+ nodes[node_name].each do |node|
42
+ root_node << node.to_xml
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ # Internal: creates an XmlSection. This shouldn't normally be called
49
+ # directly, instead the subclasses call it as they define a useful
50
+ # NODE_ORDER.
51
+ #
52
+ # name - a Symbol that corresponds to the node name in NODE_ORDER
53
+ # block - a Block that will be executed in the context of this class for
54
+ # setting up descendents.
55
+ def initialize(attributes={}, &block)
56
+ @attributes = attributes
57
+ yield self if block_given?
58
+ end
59
+
60
+ class << self
61
+ attr_writer :xml_name, :formats
62
+ attr_accessor :node_order
63
+ end
64
+
65
+ # Internal: a class method for getting the name of the XML node used when
66
+ # converting instances to XML for HMRC. If the `xml_name` has been set then
67
+ # it will be used, otherwise the class name will be used as a default.
68
+ #
69
+ # Returns a String
70
+ def self.xml_name
71
+ @xml_name || name.demodulize
72
+ end
73
+
74
+ # Internal: provides access to the formats hash, falling back on an empty
75
+ # hash by default. These formats are used by the date and monetary nodes
76
+ # for converting their values to strings HMRC will accept.
77
+ #
78
+ # Returns a Hash
79
+ def self.formats
80
+ @formats ||= {
81
+ date: "%Y-%m-%d",
82
+ money: "%.2f"
83
+ }
84
+ end
85
+
86
+ # Internal: provides access to the hash of nodes where the default for an
87
+ # unspecified key is an empty array. This stores the nodes as they are
88
+ # created with the key being the name of the node (which is the name of the
89
+ # method called to set it) and the value being an array of all the values
90
+ # set on this node in the order they are set.
91
+ #
92
+ # Returns a Hash
93
+ def nodes
94
+ @nodes ||= Hash.new { |h, k| h[k] = [] }
95
+ end
96
+
97
+ private
98
+
99
+ # Private: a convenience method for getting the format string for a given
100
+ # key.
101
+ #
102
+ # Returns a format String
103
+ # Raises a KeyError if the requested format is not found
104
+ def format_for(type)
105
+ self.class.formats.fetch(type)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,223 @@
1
+ require 'bigdecimal'
2
+ require 'active_support'
3
+ require 'active_support/core_ext/string/inflections'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ require 'hermod/xml_node'
7
+
8
+ module Hermod
9
+
10
+ # Internal: the base class from which all Hermod errors inherit
11
+ XmlError = Class.new(StandardError)
12
+
13
+ # Public: the error that's raised whenever some validation or constraint
14
+ # specified on a node fails.
15
+ InvalidInputError = Class.new(XmlError)
16
+
17
+ # Public: the error that's raised when you try to define two nodes with the
18
+ # same name when building an XmlSection.
19
+ DuplicateNodeError = Class.new(XmlError)
20
+
21
+ # Public: Used to build an anonymous subclass of XmlSection with methods for
22
+ # defining nodes on that subclass of varying types used by HMRC.
23
+ class XmlSectionBuilder
24
+
25
+ ZERO = BigDecimal.new('0').freeze
26
+ BOOLEAN_VALUES = [
27
+ YES = "yes".freeze,
28
+ NO = "no".freeze,
29
+ ]
30
+
31
+ # Internal: Sets up the builder with the anonymous subclass of XmlSection.
32
+ # Don't use this directly, instead see `Hermod::XmlSection.build`
33
+ def initialize(new_class)
34
+ @new_class = new_class
35
+ @node_order = []
36
+ end
37
+
38
+ # Internal: Takes a block to build the class using the methods on this
39
+ # builder and then sets the correct node_order (to ensure the nodes are
40
+ # ordered correctly in the XML).
41
+ #
42
+ # Returns the newly built class. This should be assigned to a constant
43
+ # before use.
44
+ def build(&block)
45
+ yield self
46
+ @new_class.node_order = @node_order
47
+ @new_class
48
+ end
49
+
50
+ # Public: defines a node for sending a string to HMRC
51
+ #
52
+ # symbolic_name - the name of the node. This will become the name of the
53
+ # method on the XmlSection.
54
+ # options - a hash of options used to set up validations.
55
+ #
56
+ # Returns nothing you should rely on
57
+ def string_node(symbolic_name, options={})
58
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
59
+ @node_order << symbolic_name
60
+
61
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
62
+
63
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
64
+ if options.has_key?(:input_mutator)
65
+ value, attributes = options[:input_mutator].call(value, attributes)
66
+ end
67
+ if value.blank?
68
+ if options[:optional]
69
+ return # Don't need to add an empty node
70
+ else
71
+ raise InvalidInputError, "#{symbolic_name} isn't optional but no value was provided"
72
+ end
73
+ end
74
+ if options.has_key?(:allowed_values) && !options[:allowed_values].include?(value)
75
+ raise InvalidInputError,
76
+ "#{value.inspect} is not in the list of allowed values for #{symbolic_name}: #{options[:allowed_values].inspect}"
77
+ end
78
+ if options.has_key?(:matches) && value !~ options[:matches]
79
+ raise InvalidInputError,
80
+ "Value #{value.inspect} for #{symbolic_name} doesn't match #{options[:matches].inspect}"
81
+ end
82
+ nodes[symbolic_name] << XmlNode.new(xml_name, value.to_s, attributes).rename_attributes(options[:attributes])
83
+ end
84
+ end
85
+
86
+ # Public: defines a node for sending an integer to HMRC
87
+ #
88
+ # symbolic_name - the name of the node. This will become the name of the
89
+ # method on the XmlSection.
90
+ # options - a hash of options used to set up validations.
91
+ #
92
+ # Returns nothing you should rely on
93
+ def integer_node(symbolic_name, options={})
94
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
95
+ @node_order << symbolic_name
96
+
97
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
98
+
99
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
100
+ if options.has_key?(:range) && (options[:range][:min] > value || options[:range][:max] < value)
101
+ raise InvalidInputError,
102
+ "#{value} is outwith the allowable range for #{symbolic_name}: #{options[:range][:min]} - #{options[:range][:max]}"
103
+ end
104
+ nodes[symbolic_name] << XmlNode.new(xml_name, value.to_s, attributes).rename_attributes(options[:attributes]) if value.present?
105
+ end
106
+ end
107
+
108
+ # Public: defines a node for sending a date to HMRC
109
+ #
110
+ # symbolic_name - the name of the node. This will become the name of the
111
+ # method on the XmlSection.
112
+ # options - a hash of options used to set up validations.
113
+ #
114
+ # Returns nothing you should rely on
115
+ def date_node(symbolic_name, options={})
116
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
117
+ @node_order << symbolic_name
118
+
119
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
120
+
121
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
122
+ if value.blank?
123
+ if options[:optional]
124
+ return # Don't need to add an empty node
125
+ else
126
+ raise InvalidInputError, "#{symbolic_name} isn't optional but no value was provided"
127
+ end
128
+ end
129
+ unless value.respond_to?(:strftime)
130
+ raise InvalidInputError, "#{symbolic_name} must be set to a date"
131
+ end
132
+ nodes[symbolic_name] << XmlNode.new(xml_name, value.strftime(format_for(:date)), attributes).rename_attributes(options[:attributes])
133
+ end
134
+ end
135
+
136
+ # Public: defines a node for sending a boolean to HMRC. It will only be
137
+ # sent if the boolean is true.
138
+ #
139
+ # symbolic_name - the name of the node. This will become the name of the
140
+ # method on the XmlSection.
141
+ # options - a hash of options used to set up validations.
142
+ #
143
+ # Returns nothing you should rely on
144
+ def yes_node(symbolic_name, options={})
145
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
146
+ @node_order << symbolic_name
147
+
148
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
149
+
150
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
151
+ if value
152
+ nodes[symbolic_name] << XmlNode.new(xml_name, YES, attributes).rename_attributes(options[:attributes]) if value.present?
153
+ end
154
+ end
155
+ end
156
+
157
+ # Public: defines a node for sending a boolean to HMRC. A "yes" will be
158
+ # sent if it's true and a "no" will be sent if it's false
159
+ #
160
+ # symbolic_name - the name of the node. This will become the name of the
161
+ # method on the XmlSection.
162
+ # options - a hash of options used to set up validations.
163
+ #
164
+ # Returns nothing you should rely on
165
+ def yes_no_node(symbolic_name, options={})
166
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
167
+ @node_order << symbolic_name
168
+
169
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
170
+
171
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
172
+ nodes[symbolic_name] << XmlNode.new(xml_name, value ? YES : NO, attributes).rename_attributes(options[:attributes])
173
+ end
174
+ end
175
+
176
+ # Public: defines a node for sending a monetary value to HMRC
177
+ #
178
+ # symbolic_name - the name of the node. This will become the name of the
179
+ # method on the XmlSection.
180
+ # options - a hash of options used to set up validations.
181
+ #
182
+ # Returns nothing you should rely on
183
+ def monetary_node(symbolic_name, options={})
184
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
185
+ @node_order << symbolic_name
186
+
187
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
188
+
189
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
190
+ value ||= 0 # nils are zero
191
+ if !options.fetch(:negative, true) && value < ZERO
192
+ raise InvalidInputError, "#{symbolic_name} cannot be negative"
193
+ end
194
+ # Don't allow fractional values for whole number nodes
195
+ if options[:whole_units] && value != value.to_i
196
+ raise InvalidInputError, "#{symbolic_name} must be in whole pounds"
197
+ end
198
+ # Don't include optional nodes if they're zero
199
+ if !(options[:optional] && value.zero?)
200
+ nodes[symbolic_name] << XmlNode.new(xml_name, sprintf(format_for(:money), value), attributes).rename_attributes(options[:attributes]) if value.present?
201
+ end
202
+ end
203
+ end
204
+
205
+ # Public: defines an XML parent node that wraps other nodes
206
+ #
207
+ # symbolic_name - the name of the node. This will become the name of the
208
+ # method on the XmlSection.
209
+ # options - a hash of options used to set up validations.
210
+ #
211
+ # Returns nothing you should rely on
212
+ def parent_node(symbolic_name, options={})
213
+ raise DuplicateNodeError, "#{symbolic_name} is already defined" if @node_order.include? symbolic_name
214
+ @node_order << symbolic_name
215
+
216
+ xml_name = options.fetch(:xml_name, symbolic_name.to_s.camelize)
217
+
218
+ @new_class.send :define_method, symbolic_name do |value, attributes={}|
219
+ nodes[symbolic_name] << XmlNode.new(xml_name, value, attributes).rename_attributes(options[:attributes]) if value.present?
220
+ end
221
+ end
222
+ end
223
+ end
data/lib/hermod.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'hermod/xml_section'
2
+
3
+ # The namespace for the gem
4
+ module Hermod
5
+ end
@@ -0,0 +1,27 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ DateXml = XmlSection.build(formats: {date: "%Y-%m-%d"}) do |builder|
7
+ builder.date_node :date_of_birth
8
+ builder.date_node :anniversary
9
+ end
10
+
11
+ describe "Date nodes" do
12
+ subject do
13
+ DateXml.new do |dummy|
14
+ dummy.date_of_birth Date.new(1988, 8, 13)
15
+ end
16
+ end
17
+
18
+ it "should format the date with the given date string" do
19
+ value_of_node("DateOfBirth").must_equal "1988-08-13"
20
+ end
21
+
22
+ it "should raise an error if given something that isn't a date" do
23
+ proc { subject.anniversary "yesterday" }.must_raise InvalidInputError
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ MonetaryXml = XmlSection.build(formats: {money: "%.2f"}) do |builder|
7
+ builder.monetary_node :pay
8
+ builder.monetary_node :tax, optional: true
9
+ builder.monetary_node :ni, xml_name: "NI", negative: false
10
+ builder.monetary_node :pension, whole_units: true
11
+ end
12
+
13
+ describe "Monetary nodes" do
14
+ subject do
15
+ MonetaryXml.new do |dummy|
16
+ dummy.pay 1000
17
+ dummy.tax 0
18
+ end
19
+ end
20
+
21
+ it "should format values with the provided format string" do
22
+ value_of_node("Pay").must_equal "1000.00"
23
+ end
24
+
25
+ it "should not include optional nodes if they're zero" do
26
+ number_of_nodes("Tax").must_equal 0
27
+ end
28
+
29
+ it "should use xml_name as the node name if provided" do
30
+ subject.ni 100
31
+ number_of_nodes("NI").must_equal 1
32
+ end
33
+
34
+ it "should raise an error if given a negative number for a field that cannot be negative" do
35
+ ex = proc { subject.ni -100 }.must_raise InvalidInputError
36
+ ex.message.must_equal "ni cannot be negative"
37
+ end
38
+
39
+ it "should allow negative numbers for fields by default" do
40
+ subject.pension(-100)
41
+ value_of_node("Pension").must_equal "-100.00"
42
+ end
43
+
44
+ it "should not allow pennies for whole unit nodes" do
45
+ ex = proc { subject.pension BigDecimal.new("12.34") }.must_raise InvalidInputError
46
+ ex.message.must_equal "pension must be in whole pounds"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ ParentXml = XmlSection.build do |builder|
5
+ builder.parent_node :inner
6
+ end
7
+
8
+ InnerXml = XmlSection.build do |builder|
9
+ builder.string_node :inside
10
+ end
11
+
12
+ describe XmlSection do
13
+ describe "Parent XML nodes" do
14
+ subject do
15
+ ParentXml.new do |outer|
16
+ outer.inner(InnerXml.new do |inner|
17
+ inner.inside "layered like an onion"
18
+ end)
19
+ end
20
+ end
21
+
22
+ it "should correctly wrap the inner XML" do
23
+ expected = "<ParentXml>\n <InnerXml>\n <Inside>layered like an onion</Inside>\n </InnerXml>\n</ParentXml>"
24
+ subject.to_xml.to_s.must_equal expected
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,70 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ StringXml = XmlSection.build do |builder|
7
+ builder.string_node :greeting
8
+ builder.string_node :name, optional: true
9
+ builder.string_node :title, matches: /\ASir|Dame\z/, attributes: {masculine: "Male"}
10
+ builder.string_node :required
11
+ builder.string_node :gender, input_mutator: (lambda do |value, attributes|
12
+ [value == "Male" ? "M" : "F", attributes]
13
+ end)
14
+ builder.string_node :mood, allowed_values: %w(Happy Sad Hangry)
15
+ end
16
+
17
+ describe "String nodes" do
18
+ subject do
19
+ StringXml.new do |string_xml|
20
+ string_xml.greeting "Hello"
21
+ string_xml.name "World"
22
+ end
23
+ end
24
+
25
+ it "should set node contents correctly" do
26
+ value_of_node("Greeting").must_equal "Hello"
27
+ end
28
+
29
+ it "should allow values that pass the regex validation" do
30
+ subject.title "Sir"
31
+ value_of_node("Title").must_equal "Sir"
32
+ end
33
+
34
+ it "should raise an error when the regex validation fails" do
35
+ ex = proc { subject.title "Laird" }.must_raise InvalidInputError
36
+ ex.message.must_equal %{Value "Laird" for title doesn't match /\\ASir|Dame\\z/}
37
+ end
38
+
39
+ it "should require all non-optional nodes to have content" do
40
+ ex = proc { subject.required "" }.must_raise InvalidInputError
41
+ ex.message.must_equal "required isn't optional but no value was provided"
42
+ end
43
+
44
+ it "should apply changes to the inputs if a input_mutator is provided" do
45
+ subject.gender "Male"
46
+ value_of_node("Gender").must_equal "M"
47
+ end
48
+
49
+ it "should restrict values to those in the list of allowed values if such a list is provided" do
50
+ subject.mood "Hangry"
51
+ value_of_node("Mood").must_equal "Hangry"
52
+ end
53
+
54
+ it "should raise an error if the value is not in the list of allowed values" do
55
+ ex = proc { subject.mood "Jubilant" }.must_raise InvalidInputError
56
+ ex.message.must_equal %{"Jubilant" is not in the list of allowed values for mood: ["Happy", "Sad", "Hangry"]}
57
+ end
58
+
59
+ it "should use the given keys for attributes" do
60
+ subject.title "Sir", masculine: "no"
61
+ attributes_for_node("Title").keys.first.must_equal "Male"
62
+ attributes_for_node("Title")["Male"].value.must_equal "no"
63
+ end
64
+
65
+ it "should raise an error if given an attribute that isn't expected" do
66
+ proc { subject.title "Sir", knight: "yes" }.must_raise KeyError
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,36 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ YesNoXml = XmlSection.build do |builder|
7
+ builder.yes_no_node :awesome
8
+ end
9
+
10
+ describe "Yes/No nodes" do
11
+ describe "when true" do
12
+ subject do
13
+ YesNoXml.new do |yes_no_xml|
14
+ yes_no_xml.awesome true
15
+ end
16
+ end
17
+
18
+ it "should include the node with yes as the contents" do
19
+ value_of_node("Awesome").must_equal "yes"
20
+ end
21
+ end
22
+
23
+ describe "when not true" do
24
+ subject do
25
+ YesNoXml.new do |yes_no_xml|
26
+ yes_no_xml.awesome false
27
+ end
28
+ end
29
+
30
+ it "should include the node with no as the contents" do
31
+ value_of_node("Awesome").must_equal "no"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,36 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+ describe XmlSection do
5
+
6
+ YesXml = XmlSection.build do |builder|
7
+ builder.yes_node :awesome
8
+ end
9
+
10
+ describe "Yes Only Nodes" do
11
+ describe "when true" do
12
+ subject do
13
+ YesXml.new do |yes_xml|
14
+ yes_xml.awesome true
15
+ end
16
+ end
17
+
18
+ it "should include the node with yes as the contents" do
19
+ value_of_node("Awesome").must_equal "yes"
20
+ end
21
+ end
22
+
23
+ describe "when not true" do
24
+ subject do
25
+ YesXml.new do |yes_xml|
26
+ yes_xml.awesome false
27
+ end
28
+ end
29
+
30
+ it "should not include the node" do
31
+ number_of_nodes("Awesome").must_equal 0
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,95 @@
1
+ require "minitest_helper"
2
+
3
+ module Hermod
4
+
5
+ NamedXML = XmlSection.build(xml_name: "Testing") { |builder| }
6
+ UnnamedXML = XmlSection.build { |builder| }
7
+ OrderingXML = XmlSection.build do |builder|
8
+ builder.string_node :first
9
+ builder.string_node :repeated
10
+ builder.string_node :last
11
+ end
12
+ FormattedXML = XmlSection.build do |builder|
13
+ builder.date_node :birthday
14
+ builder.monetary_node :allowance
15
+ end
16
+
17
+ describe XmlSection do
18
+ describe "building an XML generating class with no arguments" do
19
+ subject do
20
+ UnnamedXML.new
21
+ end
22
+
23
+ it "should use the class name as the XML node name" do
24
+ subject.to_xml.name.must_equal "UnnamedXML"
25
+ end
26
+ end
27
+
28
+ describe "building an XML generating class with a custom node name" do
29
+ subject do
30
+ NamedXML.new
31
+ end
32
+
33
+ it "should use the class name as the XML node name" do
34
+ subject.to_xml.name.must_equal "Testing"
35
+ end
36
+ end
37
+
38
+ describe "default formats" do
39
+ subject do
40
+ FormattedXML.new do |formatted|
41
+ formatted.birthday Date.new(1988, 8, 13)
42
+ formatted.allowance BigDecimal.new("20")
43
+ end
44
+ end
45
+
46
+ it "formats dates in yyyy-mm-dd form" do
47
+ value_of_node("Birthday").must_equal("1988-08-13")
48
+ end
49
+
50
+ it "formats money to two decimal places" do
51
+ value_of_node("Allowance").must_equal("20.00")
52
+ end
53
+ end
54
+
55
+ describe "#to_xml" do
56
+ subject do
57
+ OrderingXML.new do |ordering|
58
+ ordering.repeated "beta"
59
+ ordering.last "epsilon"
60
+ ordering.repeated "gamma"
61
+ ordering.first "alpha"
62
+ end
63
+ end
64
+
65
+ it "should order nodes by the order they were defined when the class was built" do
66
+ node_by_index(0).name.must_equal "First"
67
+ node_by_index(0).content.must_equal "alpha"
68
+
69
+ node_by_index(1).name.must_equal "Repeated"
70
+ node_by_index(1).content.must_equal "beta"
71
+
72
+ node_by_index(-1).name.must_equal "Last"
73
+ node_by_index(-1).content.must_equal "epsilon"
74
+ end
75
+
76
+ it "should order nodes called multiple times in the order they were called" do
77
+ node_by_index(1).name.must_equal "Repeated"
78
+ node_by_index(1).content.must_equal "beta"
79
+
80
+ node_by_index(2).name.must_equal "Repeated"
81
+ node_by_index(2).content.must_equal "gamma"
82
+ end
83
+
84
+ it "should order nodes at XML generation time, not at call time" do
85
+ subject.repeated "delta"
86
+
87
+ node_by_index(-2).name.must_equal "Repeated"
88
+ node_by_index(-2).content.must_equal "delta"
89
+
90
+ node_by_index(-1).name.must_equal "Last"
91
+ node_by_index(-1).content.must_equal "epsilon"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,7 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Hermod do
4
+ it "should have a version number" do
5
+ Hermod::VERSION.wont_be_nil
6
+ end
7
+ end