Refactoring of the model classes, created unit-tests.
authorFrantišek Dvořák <valtri@civ.zcu.cz>
Tue, 21 Jun 2016 22:02:54 +0000 (00:02 +0200)
committerFrantišek Dvořák <valtri@civ.zcu.cz>
Tue, 21 Jun 2016 22:26:02 +0000 (00:26 +0200)
12 files changed:
.rubocop.yml
.travis.yml
Gemfile.devel
application.rb
lib/nebula.rb
models/helpers/nowhash.rb [new file with mode: 0644]
models/network.rb
models/range.rb
spec/models/network_spec.rb [new file with mode: 0644]
spec/models/range_spec.rb [new file with mode: 0644]
spec/spec_helper.rb [new file with mode: 0644]
swagger.yaml

index f297369..41683a4 100644 (file)
@@ -1,10 +1,6 @@
 AllCops:
   DisplayCopNames: true
   DisplayStyleGuide: true
-  Exclude:
-   # exclude generated code (maybe we'll rewrite it anyway one day)
-   - models/network.rb
-   - models/range.rb
 
 # Assignment Branch Condition size for initialize is too high
 # Perceived complexity for deep_merge is too high
index 6c696f5..808e015 100644 (file)
@@ -10,4 +10,5 @@ rvm:
 
 script:
   - bundle exec rubocop
+  - bundle exec rspec
   - bundle exec ruby -rminitest/autorun -Ilib:test -e 'Dir.glob "./test/*_test.rb", &method(:require)'
index 7ba90f6..f63e521 100644 (file)
@@ -1,11 +1,13 @@
 source 'https://rubygems.org'
 
+gem 'ipaddress'
 gem 'opennebula'
 gem 'sinatra'
 gem 'sinatra-cross_origin'
 # recommended for sinatra
 gem 'thin'
 
-gem 'rubocop', group: :development, require: false
-gem 'rack-test', group: :development
 gem 'minitest', group: :development
+gem 'rack-test', group: :development
+gem 'rspec', group: :development
+gem 'rubocop', group: :development, require: false
index bd44200..3c03db5 100644 (file)
@@ -1,6 +1,6 @@
 require 'logger'
 
-Dir['./models/*.rb'].each do |file|
+Dir['./models/helpers/*.rb', './models/*.rb'].each do |file|
   require file
 end
 require './version'
index a5c6ca0..7c8704d 100644 (file)
@@ -1,5 +1,6 @@
 require 'opennebula'
 require 'yaml'
+require 'ipaddress'
 
 module Now
 
@@ -138,7 +139,7 @@ module Now
       mask = addr_size - Math.log(size, 2).ceil
       logger.debug "[parse_range] id=#{id}, address=#{ip}/#{mask} (size #{size})"
 
-      return Now::Range.new(address: "#{ip}/#{mask}", allocation: 'dynamic')
+      return Now::Range.new(address: IPAddress.parse("#{ip}/#{mask}"), allocation: 'dynamic')
     end
 
     def parse_ranges(vn_id, vn)
@@ -166,7 +167,7 @@ module Now
     end
 
     def parse_network(vn)
-      logger.debug "[parse_network] #{vn.to_hash}"
+      logger.debug "[parse_network] #{vn.to_xml}"
 
       id = vn.id
       title = vn.name
diff --git a/models/helpers/nowhash.rb b/models/helpers/nowhash.rb
new file mode 100644 (file)
index 0000000..e13d257
--- /dev/null
@@ -0,0 +1,45 @@
+module Now
+
+  # Generic hash class with custom accessors and helper methods
+  class NowHash < ::Hash
+    def self.my_accessor(*keys)
+      keys.each do |key|
+        define_method(key) do
+          return nil if !key?(key)
+          fetch(key)
+        end
+        define_method("#{key}=") do |new_value|
+          if new_value.nil?
+            delete(key)
+          else
+            store(key, new_value)
+          end
+        end
+      end
+    end
+
+    def initialize(parameters = {})
+      parameters.select! { |_k, v| !v.nil? }
+      replace(parameters)
+    end
+
+    # Conversion of the data structure to hash. Arrays and hashes are browsed, the leafs are converted by calling to_hash method or directly copied.
+    # @param [Object] value Any valid value
+    # @return [Hash] Returns the value in the form of hash
+    def _to_hash(value)
+      if value.is_a?(Array)
+        value.map { |v| _to_hash(v) }
+      # beware we're the Hash!!!
+      elsif value.is_a?(Hash) && !value.is_a?(Now::NowHash)
+        {}.tap do |hash|
+          value.each { |k, v| hash[k] = _to_hash(v) }
+        end
+      elsif value.respond_to? :to_hash
+        value.to_hash
+      else
+        value
+      end
+    end
+  end
+
+end
index ba966db..23e2b40 100644 (file)
-=begin
-Network Orchestrator API
-
-OpenAPI spec version: 0.0.0
-
-Partially generated by: https://github.com/swagger-api/swagger-codegen.git
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-=end
-
 require 'date'
 
 module Now
+
   # Network object
-  class Network
+  class Network < NowHash
     # OpenNebula ID
-    attr_accessor :id
+    my_accessor :id
 
     # Network title
-    attr_accessor :title
+    my_accessor :title
 
     # Network summary
-    attr_accessor :description
+    my_accessor :description
 
     # Owner
-    attr_accessor :user
+    my_accessor :user
 
     # VLAN ID
-    attr_accessor :vlan
-
-    attr_accessor :range
-
-    # Network state (active, inactive, error)
-    attr_accessor :state
-
-    # Availability zone (cluster)
-    attr_accessor :zone
-
-
-    # Attribute mapping from ruby-style variable name to JSON key.
-    def self.attribute_map
-      {
-        :'id' => :'id',
-        :'title' => :'title',
-        :'description' => :'description',
-        :'user' => :'user',
-        :'vlan' => :'vlan',
-        :'range' => :'range',
-        :'state' => :'state',
-        :'zone' => :'zone'
-      }
-    end
+    my_accessor :vlan
 
-    # Attribute type mapping.
-    def self.swagger_types
-      {
-        :'id' => :'Integer',
-        :'title' => :'String',
-        :'description' => :'String',
-        :'user' => :'String',
-        :'vlan' => :'Integer',
-        :'range' => :'Range',
-        :'state' => :'String',
-        :'zone' => :'String'
-      }
+    # IP address range (reader)
+    def range
+      return nil if !key?(:range)
+      fetch(:range)
     end
 
-    # Initializes the object
-    # @param [Hash] attributes Model attributes in the form of hash
-    def initialize(attributes = {})
-      return unless attributes.is_a?(Hash)
-
-      # convert string to symbol for hash key
-      attributes = attributes.each_with_object({}){|(k,v), h| h[k.to_sym] = v}
-
-      if attributes.has_key?(:'id')
-        self.id = attributes[:'id']
-      end
-
-      if attributes.has_key?(:'title')
-        self.title = attributes[:'title']
-      end
-
-      if attributes.has_key?(:'description')
-        self.description = attributes[:'description']
-      end
-
-      if attributes.has_key?(:'user')
-        self.user = attributes[:'user']
+    # IP address range (writer)
+    def range=(new_value)
+      if !valid_range?(new_value)
+        raise NowError.new(500), 'Invalid range type'
       end
+      store(:range, new_value)
+    end
 
-      if attributes.has_key?(:'vlan')
-        self.vlan = attributes[:'vlan']
-      end
+    # Network state (active, inactive, error)
+    my_accessor :state
 
-      if attributes.has_key?(:'range')
-        self.range = attributes[:'range']
-      end
+    # Availability zone (cluster)
+    my_accessor :zone
 
-      if attributes.has_key?(:'state')
-        self.state = attributes[:'state']
+    def initialize(parameters = {})
+      if !parameters.key?(:id)
+        raise NowError.new(500), 'ID required in network object'
       end
-
-      if attributes.has_key?(:'zone')
-        self.zone = attributes[:'zone']
+      if parameters.key?(:range) && !valid_range?(parameters[:range])
+        raise NowError.new(500), 'Valid range object required in network object'
       end
-
-    end
-
-    # Show invalid properties with the reasons. Usually used together with valid?
-    # @return Array for valid properies with the reasons
-    def list_invalid_properties
-      invalid_properties = Array.new
-      return invalid_properties
+      super
     end
 
     # Check to see if the all the properties in the model are valid
     # @return true if the model is valid
     def valid?
-      return false if @id.nil?
+      return false if id.nil?
+      return false if !valid_range?(range)
       return true
     end
 
     # Checks equality by comparing each attribute.
     # @param [Object] Object to be compared
-    def ==(o)
-      return true if self.equal?(o)
-      self.class == o.class &&
-          id == o.id &&
-          title == o.title &&
-          description == o.description &&
-          user == o.user &&
-          vlan == o.vlan &&
-          range == o.range &&
-          state == o.state &&
-          zone == o.zone
+    def ==(other)
+      return true if equal?(other)
+      self.class == other.class &&
+        id == other.id &&
+        title == other.title &&
+        description == other.description &&
+        user == other.user &&
+        vlan == other.vlan &&
+        range == other.range &&
+        state == other.state &&
+        zone == other.zone
     end
 
     # @see the `==` method
     # @param [Object] Object to be compared
-    def eql?(o)
-      self == o
+    def eql?(other)
+      self == other
     end
 
     # Calculates hash code according to all attributes.
@@ -159,110 +84,28 @@ module Now
       [id, title, description, user, vlan, range, state, zone].hash
     end
 
-    # Builds the object from hash
-    # @param [Hash] attributes Model attributes in the form of hash
-    # @return [Object] Returns the model itself
-    def build_from_hash(attributes)
-      return nil unless attributes.is_a?(Hash)
-      self.class.swagger_types.each_pair do |key, type|
-        if type =~ /^Array<(.*)>/i
-          # check to ensure the input is an array given that the the attribute
-          # is documented as an array but the input is not
-          if attributes[self.class.attribute_map[key]].is_a?(Array)
-            self.send("#{key}=", attributes[self.class.attribute_map[key]].map{ |v| _deserialize($1, v) } )
-          end
-        elsif !attributes[self.class.attribute_map[key]].nil?
-          self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]]))
-        end # or else data not found in attributes(hash), not an issue as the data can be optional
-      end
-
-      self
-    end
-
-    # Deserializes the data based on type
-    # @param string type Data type
-    # @param string value Value to be deserialized
-    # @return [Object] Deserialized data
-    def _deserialize(type, value)
-      case type.to_sym
-      when :DateTime
-        DateTime.parse(value)
-      when :Date
-        Date.parse(value)
-      when :String
-        value.to_s
-      when :Integer
-        value.to_i
-      when :Float
-        value.to_f
-      when :BOOLEAN
-        if value.to_s =~ /^(true|t|yes|y|1)$/i
-          true
-        else
-          false
-        end
-      when :Object
-        # generic object (usually a Hash), return directly
-        value
-      when /\AArray<(?<inner_type>.+)>\z/
-        inner_type = Regexp.last_match[:inner_type]
-        value.map { |v| _deserialize(inner_type, v) }
-      when /\AHash<(?<k_type>.+), (?<v_type>.+)>\z/
-        k_type = Regexp.last_match[:k_type]
-        v_type = Regexp.last_match[:v_type]
-        {}.tap do |hash|
-          value.each do |k, v|
-            hash[_deserialize(k_type, k)] = _deserialize(v_type, v)
-          end
-        end
-      else # model
-        temp_model = Now.const_get(type).new
-        temp_model.build_from_hash(value)
-      end
-    end
-
     # Returns the string representation of the object
     # @return [String] String presentation of the object
     def to_s
       to_hash.to_s
     end
 
-    # to_body is an alias to to_hash (backward compatibility)
-    # @return [Hash] Returns the object in the form of hash
-    def to_body
-      to_hash
-    end
-
     # Returns the object in the form of hash
     # @return [Hash] Returns the object in the form of hash
     def to_hash
       hash = {}
-      self.class.attribute_map.each_pair do |attr, param|
-        value = self.send(attr)
-        next if value.nil?
-        hash[param] = _to_hash(value)
+      each_pair do |attr, value|
+        hash[attr] = _to_hash(value)
       end
-      hash
-    end
 
-    # Outputs non-array value in the form of hash
-    # For object, use to_hash. Otherwise, just return the value
-    # @param [Object] value Any valid value
-    # @return [Hash] Returns the value in the form of hash
-    def _to_hash(value)
-      if value.is_a?(Array)
-        value.compact.map{ |v| _to_hash(v) }
-      elsif value.is_a?(Hash)
-        {}.tap do |hash|
-          value.each { |k, v| hash[k] = _to_hash(v) }
-        end
-      elsif value.respond_to? :to_hash
-        value.to_hash
-      else
-        value
-      end
+      return hash
     end
 
+    private
+
+    def valid_range?(value)
+      value.nil? || value.is_a?(Now::Range)
+    end
   end
 
 end
index 0b4f556..0a17712 100644 (file)
@@ -1,96 +1,56 @@
-=begin
-Network Orchestrator API
-
-OpenAPI spec version: 0.0.0
-
-Partially generated by: https://github.com/swagger-api/swagger-codegen.git
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-
-=end
-
-require 'date'
+require 'ipaddress'
 
 module Now
-  # Address range
-  class Range
-    # Address range (CIDR notation)
-    attr_accessor :address
-
-    # Address allocation type (static, dynamic)
-    attr_accessor :allocation
 
+  # Address range
+  class Range < NowHash
 
-    # Attribute mapping from ruby-style variable name to JSON key.
-    def self.attribute_map
-      {
-        :'address' => :'address',
-        :'allocation' => :'allocation'
-      }
+    # Address range in CIDR notation (reader)
+    def address
+      fetch(:address)
     end
 
-    # Attribute type mapping.
-    def self.swagger_types
-      {
-        :'address' => :'String',
-        :'allocation' => :'String'
-      }
+    # Address range in CIDR notation (writer)
+    def address=(new_value)
+      if !valid_address?(new_value)
+        raise NowError.new(500), 'Internal error: Invalid IP network address'
+      end
+      store(:address, new_value)
     end
 
-    # Initializes the object
-    # @param [Hash] attributes Model attributes in the form of hash
-    def initialize(attributes = {})
-      return unless attributes.is_a?(Hash)
-
-      # convert string to symbol for hash key
-      attributes = attributes.each_with_object({}){|(k,v), h| h[k.to_sym] = v}
+    # Address allocation type (static, dynamic)
+    my_accessor :allocation
 
-      if attributes.has_key?(:'address')
-        self.address = attributes[:'address']
+    def initialize(parameters = {})
+      if !parameters.key?(:address)
+        raise NowError.new(500), 'Internal error: IP network address required'
       end
-
-      if attributes.has_key?(:'allocation')
-        self.allocation = attributes[:'allocation']
+      if !valid_address?(parameters[:address])
+        raise NowError.new(500), 'Internal error: Invalid IP network address'
       end
-
-    end
-
-    # Show invalid properties with the reasons. Usually used together with valid?
-    # @return Array for valid properies with the reasons
-    def list_invalid_properties
-      invalid_properties = Array.new
-      return invalid_properties
+      super
     end
 
     # Check to see if the all the properties in the model are valid
     # @return true if the model is valid
     def valid?
+      return false if !valid_address?(address)
       return true
     end
 
     # Checks equality by comparing each attribute.
     # @param [Object] Object to be compared
-    def ==(o)
-      return true if self.equal?(o)
-      self.class == o.class &&
-          address == o.address &&
-          allocation == o.allocation
+    def ==(other)
+      return true if equal?(other)
+      self.class == other.class &&
+        address == other.address &&
+        allocation == other.allocation
     end
 
     # @see the `==` method
     # @param [Object] Object to be compared
-    def eql?(o)
-      self == o
+    def eql?(other)
+      self == other
     end
 
     # Calculates hash code according to all attributes.
@@ -99,108 +59,30 @@ module Now
       [address, allocation].hash
     end
 
-    # Builds the object from hash
-    # @param [Hash] attributes Model attributes in the form of hash
-    # @return [Object] Returns the model itself
-    def build_from_hash(attributes)
-      return nil unless attributes.is_a?(Hash)
-      self.class.swagger_types.each_pair do |key, type|
-        if type =~ /^Array<(.*)>/i
-          # check to ensure the input is an array given that the the attribute
-          # is documented as an array but the input is not
-          if attributes[self.class.attribute_map[key]].is_a?(Array)
-            self.send("#{key}=", attributes[self.class.attribute_map[key]].map{ |v| _deserialize($1, v) } )
-          end
-        elsif !attributes[self.class.attribute_map[key]].nil?
-          self.send("#{key}=", _deserialize(type, attributes[self.class.attribute_map[key]]))
-        end # or else data not found in attributes(hash), not an issue as the data can be optional
-      end
-
-      self
-    end
-
-    # Deserializes the data based on type
-    # @param string type Data type
-    # @param string value Value to be deserialized
-    # @return [Object] Deserialized data
-    def _deserialize(type, value)
-      case type.to_sym
-      when :DateTime
-        DateTime.parse(value)
-      when :Date
-        Date.parse(value)
-      when :String
-        value.to_s
-      when :Integer
-        value.to_i
-      when :Float
-        value.to_f
-      when :BOOLEAN
-        if value.to_s =~ /^(true|t|yes|y|1)$/i
-          true
-        else
-          false
-        end
-      when :Object
-        # generic object (usually a Hash), return directly
-        value
-      when /\AArray<(?<inner_type>.+)>\z/
-        inner_type = Regexp.last_match[:inner_type]
-        value.map { |v| _deserialize(inner_type, v) }
-      when /\AHash<(?<k_type>.+), (?<v_type>.+)>\z/
-        k_type = Regexp.last_match[:k_type]
-        v_type = Regexp.last_match[:v_type]
-        {}.tap do |hash|
-          value.each do |k, v|
-            hash[_deserialize(k_type, k)] = _deserialize(v_type, v)
-          end
-        end
-      else # model
-        temp_model = Now.const_get(type).new
-        temp_model.build_from_hash(value)
-      end
-    end
-
     # Returns the string representation of the object
     # @return [String] String presentation of the object
     def to_s
       to_hash.to_s
     end
 
-    # to_body is an alias to to_hash (backward compatibility)
-    # @return [Hash] Returns the object in the form of hash
-    def to_body
-      to_hash
-    end
-
     # Returns the object in the form of hash
     # @return [Hash] Returns the object in the form of hash
     def to_hash
-      hash = {}
-      self.class.attribute_map.each_pair do |attr, param|
-        value = self.send(attr)
-        next if value.nil?
-        hash[param] = _to_hash(value)
+      h = {}
+      if key?(:address)
+        h[:address] = "#{address}/#{address.prefix}"
       end
-      hash
+      if key?(:allocation)
+        h[:allocation] = allocation
+      end
+
+      return h
     end
 
-    # Outputs non-array value in the form of hash
-    # For object, use to_hash. Otherwise, just return the value
-    # @param [Object] value Any valid value
-    # @return [Hash] Returns the value in the form of hash
-    def _to_hash(value)
-      if value.is_a?(Array)
-        value.compact.map{ |v| _to_hash(v) }
-      elsif value.is_a?(Hash)
-        {}.tap do |hash|
-          value.each { |k, v| hash[k] = _to_hash(v) }
-        end
-      elsif value.respond_to? :to_hash
-        value.to_hash
-      else
-        value
-      end
+    private
+
+    def valid_address?(value)
+      !value.nil? && value.is_a?(IPAddress)
     end
 
   end
diff --git a/spec/models/network_spec.rb b/spec/models/network_spec.rb
new file mode 100644 (file)
index 0000000..c85a010
--- /dev/null
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe Now::Network do
+  context '#type check' do
+    it 'no id raises NowError' do
+      expect { Now::Network.new }.to raise_error(Now::NowError)
+    end
+    it 'string range raises NowError' do
+      expect { Now::Network.new(id: 0, range: 'eee') }.to raise_error(Now::NowError)
+    end
+  end
+
+  context '#no addess range' do
+    let(:network) { Now::Network.new(id: 0) }
+    let(:range) { Now::Range.new(address: IPAddress.parse('fd00::/8')) }
+    let(:range2) { Now::Range.new(address: IPAddress.parse('fd00::/8')) }
+    let(:hash) { { id: 0 } }
+    let(:hash_rich) do
+      {
+        id: 1,
+        title: 'Title 1',
+        description: 'Description 1',
+        user: 'spike',
+        vlan: 100,
+        range: {
+          address: 'fd00::/8',
+        },
+        zone: '2',
+      }
+    end
+
+    it 'is a network' do
+      expect(network).to be_kind_of Now::Network
+    end
+    it 'address range is nil' do
+      expect(network.range).to be nil
+    end
+    it 'is valid' do
+      expect(network.valid?).to be true
+    end
+    it 'still valid with addess range' do
+      network.range = range
+      expect(network.valid?).to be true
+    end
+    it 'attributes can be set, rich to_hash works' do
+      network.id = 1
+      network.title = 'Title 1'
+      network.description = 'Description 1'
+      network.user = 'spike'
+      network.vlan = 100
+      network.range = range
+      network.zone = '2'
+
+      expect(network.id).to eq(1)
+      expect(network.title).to eq('Title 1')
+      expect(network.description).to eq('Description 1')
+      expect(network.user).to eq('spike')
+      expect(network.vlan).to eq(100)
+      expect(network.range).to eq(range2)
+      expect(network.zone).to eq('2')
+
+      expect(network.to_hash).to eq(hash_rich)
+    end
+    it 'to_hash works' do
+      expect(network.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('192.168.0.1/24')) }
+    let(:network) { Now::Network.new(id: 0, range: range) }
+    let(:hash) do
+      {
+        id: 0,
+        range: {
+          address: '192.168.0.1/24',
+        },
+      }
+    end
+
+    it 'is a network' do
+      expect(network).to be_kind_of Now::Network
+    end
+    it 'is valid' do
+      expect(network.valid?).to be true
+    end
+    it 'still valid with nil address range' do
+      network.range = nil
+      expect(network.valid?).to be true
+    end
+    it 'to_hash works' do
+      expect(network.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic IPv6' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('fd00::/8')) }
+    let(:network) { Now::Network.new(id: 1, range: range) }
+    let(:hash) do
+      {
+        id: 1,
+        range: {
+          address: 'fd00::/8',
+        }
+      }
+    end
+    it 'is a network' do
+      expect(network).to be_kind_of Now::Network
+    end
+    it 'is valid' do
+      expect(network.valid?).to be true
+    end
+    it 'still valid with nil address range' do
+      network.range = nil
+      expect(network.valid?).to be true
+    end
+    it 'to_hash works' do
+      expect(network.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic set' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('172.16.0.0/12')) }
+    let(:network) do
+      n = Now::Network.new(id: 2)
+      n.range = range
+      n.title = 'Title'
+      n.description = 'Description'
+      n.user = 'fluttershy'
+      n
+    end
+    let(:hash) do
+      {
+        id: 2,
+        title: 'Title',
+        description: 'Description',
+        user: 'fluttershy',
+        range: {
+          address: '172.16.0.0/12',
+        },
+      }
+    end
+
+    it 'is a network' do
+      expect(network).to be_kind_of Now::Network
+    end
+    it 'is valid' do
+      expect(network.valid?).to be true
+    end
+    it 'still valid with nil address range' do
+      network.range = nil
+      expect(network.valid?).to be true
+    end
+    it 'to_hash works' do
+      expect(network.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic IPv6 set' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('fd00::/8')) }
+    let(:network) do
+      n = Now::Network.new(id: 2)
+      n.range = range
+      n.title = 'Title'
+      n.description = 'Description'
+      n.user = 'fluttershy'
+      n
+    end
+    let(:hash) do
+      {
+        id: 2,
+        title: 'Title',
+        description: 'Description',
+        user: 'fluttershy',
+        range: {
+          address: 'fd00::/8',
+        },
+      }
+    end
+
+    it 'is a network' do
+      expect(network).to be_kind_of Now::Network
+    end
+    it 'is valid' do
+      expect(network.valid?).to be true
+    end
+    it 'still valid with nil address range' do
+      network.range = nil
+      expect(network.valid?).to be true
+    end
+    it 'to_hash works' do
+      expect(network.to_hash).to eq(hash)
+    end
+  end
+end
diff --git a/spec/models/range_spec.rb b/spec/models/range_spec.rb
new file mode 100644 (file)
index 0000000..69523ce
--- /dev/null
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Now::Range do
+  context '#type check' do
+    it 'string address raises NowError' do
+      expect { Now::Range.new(address: 'eee') }.to raise_error(Now::NowError)
+    end
+    it 'no address raises NowError' do
+      expect { Now::Range.new }.to raise_error(Now::NowError)
+    end
+  end
+
+  context '#basic' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('192.168.0.1/24')) }
+    let(:hash) { { address: '192.168.0.1/24' } }
+
+    it 'is a range' do
+      expect(range).to be_kind_of Now::Range
+    end
+    it 'is valid' do
+      expect(range.valid?).to be true
+    end
+    it 'setting nil address raises error' do
+      expect { range.address = nil }.to raise_error(Now::NowError)
+    end
+    it 'to_hash works' do
+      expect(range.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic IPv6' do
+    let(:range) { Now::Range.new(address: IPAddress.parse('fd00::/8')) }
+    let(:hash) { { address: 'fd00::/8' } }
+
+    it 'is a range' do
+      expect(range).to be_kind_of Now::Range
+    end
+    it 'is valid' do
+      expect(range.valid?).to be true
+    end
+    it 'setting nil address raises error' do
+      expect { range.address = nil }.to raise_error(Now::NowError)
+    end
+    it 'to_hash works' do
+      expect(range.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic set' do
+    let(:range) do
+      r = Now::Range.new(address: IPAddress.parse('172.16.0.0/12'))
+      r.allocation = 'dynamic'
+      r
+    end
+    let(:hash) { { address: '172.16.0.0/12', allocation: 'dynamic' } }
+
+    it 'is a range' do
+      expect(range).to be_kind_of Now::Range
+    end
+    it 'is valid' do
+      expect(range.valid?).to be true
+    end
+    it 'setting nil address raises error' do
+      expect { range.address = nil }.to raise_error(Now::NowError)
+    end
+    it 'to_hash works' do
+      expect(range.to_hash).to eq(hash)
+    end
+  end
+
+  context '#basic IPv6 set' do
+    let(:range) do
+      r = Now::Range.new(address: IPAddress.parse('fd00::/8'))
+      r.allocation = 'dynamic'
+      r
+    end
+    let(:hash) { { address: 'fd00::/8', allocation: 'dynamic' } }
+
+    it 'is a range' do
+      expect(range).to be_kind_of Now::Range
+    end
+    it 'is valid' do
+      expect(range.valid?).to be true
+    end
+    it 'setting nil address raises error' do
+      expect { range.address = nil }.to raise_error(Now::NowError)
+    end
+    it 'to_hash works' do
+      expect(range.to_hash).to eq(hash)
+    end
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644 (file)
index 0000000..6398913
--- /dev/null
@@ -0,0 +1,5 @@
+require 'rspec'
+
+Dir['./models/helpers/*.rb', './models/*.rb', './lib/*.rb'].each do |file|
+  require file
+end
index aeebeaf..47735a4 100644 (file)
@@ -66,6 +66,8 @@ definitions:
   Range:
     description: "Address range"
     type: "object"
+    required:
+    - "address"
     properties:
       address:
         type: "string"
@@ -97,6 +99,7 @@ definitions:
         type: "integer"
         format: "int64"
       range:
+        description: "IP address range"
         $ref: "#/definitions/Range"
       zone:
         description: "Availability zone (cluster)"