require 'spec_helper'

describe Bosh::Director::DeploymentPlan::ManualNetworkSubnet do
include Bosh::Director::IpUtil
  before { @network = instance_double('Bosh::Director::DeploymentPlan::Network', name: 'net_a') }

  def make_subnet(properties, availability_zones)
    Bosh::Director::DeploymentPlan::ManualNetworkSubnet.parse(@network.name, properties, availability_zones)
  end

  def make_managed_subnet(properties, availability_zones)
    Bosh::Director::DeploymentPlan::ManualNetworkSubnet.parse(@network.name, properties, availability_zones, true)
  end

  it_behaves_like 'a subnet'

  describe '#initialize' do
    it 'should create a subnet spec' do
      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'}
        },
        [],
      )

      expect(subnet.range.to_s).to eq('192.168.0.0/24')
      expect(subnet.netmask).to eq('255.255.255.0')
      expect(subnet.gateway).to eq('192.168.0.254')
      expect(subnet.dns).to eq(nil)
    end

    it 'should create a subnet spec with restricted_ips in cidr format' do
      allow(Bosh::Director::Config).to receive(:director_ips).and_return([to_ipaddr('10.0.3.11').to_s])

      subnet = make_subnet(
        {
          'dns' => ['10.0.0.2'],
          'gateway' => '10.0.3.1',
          'range' => '10.0.3.0/24',
          'reserved' => [
           '10.0.3.0 - 10.0.3.35',
           '10.0.3.242 - 10.0.3.255']
        },
        [],
      )

      expected_restricted_ips = Set.new([to_ipaddr('10.0.3.0/27'), to_ipaddr('10.0.3.32/30'), to_ipaddr('10.0.3.242/31'), to_ipaddr('10.0.3.244/30'), to_ipaddr('10.0.3.248/29') ])

      expect(subnet.range.to_s).to eq('10.0.3.0/24')
      expect(subnet.netmask).to eq('255.255.255.0')
      expect(subnet.gateway).to eq('10.0.3.1')
      expect(subnet.dns).to eq(['10.0.0.2'])
      expect(subnet.restricted_ips).to eq(expected_restricted_ips)

    end

    it 'should create valid subnet spec for managed networks' do
      subnet = make_managed_subnet(
        {
          'name' => 'some-subnet',
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => { 'foo' => 'bar' },
        },
        [],
      )

      expect(subnet.range.to_s).to eq('192.168.0.0/24')
      expect(subnet.netmask).to eq('255.255.255.0')
      expect(subnet.gateway).to eq('192.168.0.254')
      expect(subnet.dns).to eq(nil)
    end

    it 'should fail when managed subnet has no name' do
      expect do
        make_managed_subnet(
          {
            'netmask_bits' => 24,
            'cloud_properties' => { 'foo' => 'bar' },
          },
          [],
        )
      end.to raise_error(Bosh::Director::ValidationMissingField)
    end

    it 'should create a valid managed subnet with netmask bits' do
      subnet = make_managed_subnet({
        'name' => 'subnet-name',
        'netmask_bits' => 24,
        'cloud_properties' => { 'foo' => 'bar' },
      }, [])
      expect(subnet.netmask_bits).to eq(24)
    end

    it 'should create an IPv6 subnet spec' do
      subnet = make_subnet(
        {
          'range' => 'fdab:d85c:118d:8a46::/64',
          'gateway' => 'fdab:d85c:118d:8a46::1',
          'reserved' => [
            'fdab:d85c:118d:8a46::10-fdab:d85c:118d:8a46::ff',
            'fdab:d85c:118d:8a46::101',
          ],
          'static' => [
            'fdab:d85c:118d:8a46::210-fdab:d85c:118d:8a46::2ff',
            'fdab:d85c:118d:8a46::301',
          ],
          'dns' => [
            '2001:4860:4860::8888',
            '2001:4860:4860::8844',
          ],
          'cloud_properties' => {'foo' => 'bar'}
        },
        [],
      )

      expect(subnet.range).to eq('fdab:d85c:118d:8a46:0000:0000:0000:0000')
      expect(subnet.netmask).to eq('ffff:ffff:ffff:ffff:0000:0000:0000:0000')
      expect(subnet.gateway).to eq('fdab:d85c:118d:8a46:0000:0000:0000:0001')
      expect(subnet.dns).to eq([
        "2001:4860:4860::8888",
        "2001:4860:4860::8844",
      ])
    end

    it 'should require a range' do
      expect {
        make_subnet(
          {
            'cloud_properties' => {'foo' => 'bar'},
            'gateway' => '192.168.0.254',
          },
          []
        )
      }.to raise_error(Bosh::Director::ValidationMissingField)
    end

    context 'gateway property' do
      it 'should require a gateway' do
        expect {
          make_subnet(
            {
              'range' => '192.168.0.0/24',
              'cloud_properties' => {'foo' => 'bar'},
            }, []
          )
        }.to raise_error(Bosh::Director::ValidationMissingField)
      end

      context 'when the gateway is configured to be optional' do
        it 'should not require a gateway' do
          allow(Bosh::Director::Config).to receive(:ignore_missing_gateway).and_return(true)

          expect {
            make_subnet(
              {
                'range' => '192.168.0.0/24',
                'cloud_properties' => {'foo' => 'bar'},
              },
              []
            )
          }.to_not raise_error
        end
      end
    end

    it 'default cloud properties to empty hash' do
      subnet = make_subnet(
        {

          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
        },
        []
      )
      expect(subnet.cloud_properties).to eq({})
    end

    it 'should fail when cloud properties is NOT a hash' do
      expect {
        make_subnet(
            {
                'range' => '192.168.0.0/24',
                'gateway' => '192.168.0.254',
                'cloud_properties' => 'not_hash'
            },
            []
        )
      }.to raise_error(Bosh::Director::ValidationInvalidType)
    end

    it 'should allow a gateway' do
      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'}
        },
        []
      )

      expect(subnet.gateway.base_addr).to eq('192.168.0.254')
    end

    it 'should make sure gateway is a single ip' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'gateway' => '192.168.0.254/30',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkInvalidGateway,
          /must be a single IP/)
    end

    it 'should make sure gateway is inside the subnet' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'gateway' => '190.168.0.254',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkInvalidGateway,
          /must be inside the range/)
    end

    it 'should make sure gateway is not the network id' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'gateway' => '192.168.0.0',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkInvalidGateway,
          /can't be the network id/)
    end

    it 'should make sure gateway is not the broadcast IP' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'gateway' => '192.168.0.255',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkInvalidGateway,
          /can't be the broadcast IP/)
    end

    it 'should allow DNS servers' do
      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'dns' => %w(1.2.3.4 5.6.7.8),
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'}
        },
        []
      )

      expect(subnet.dns).to eq(%w(1.2.3.4 5.6.7.8))
    end

    it 'should fail when reserved range is not valid' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'reserved' => '192.167.0.5 - 192.168.0.10',
            'gateway' => '192.168.0.254',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkReservedIpOutOfRange,
          "Reserved IP '192.167.0.5/32' is out of " +
            "network 'net_a' range")
    end

    it 'should allow the reserved range to include the gateway, broadcast and network addresses' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'reserved' => ['192.168.0.0','192.168.0.1','192.168.0.255'],
            'gateway' => '192.168.0.1',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to_not raise_error
    end

    it 'should fail when the static IP is not valid' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'static' => '192.167.0.5 - 192.168.0.10',
            'gateway' => '192.168.0.254',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkStaticIpOutOfRange,
          "Static IP '192.167.0.5/32' is out of " +
            "network 'net_a' range")
    end

    it 'should fail when the static IP is in reserved range' do
      expect {
        make_subnet(
          {
            'range' => '192.168.0.0/24',
            'reserved' => '192.168.0.5 - 192.168.0.10',
            'static' => '192.168.0.5',
            'gateway' => '192.168.0.254',
            'cloud_properties' => {'foo' => 'bar'}
          },
          []
        )
      }.to raise_error(Bosh::Director::NetworkStaticIpOutOfRange,
          "Static IP '192.168.0.5/32' is in network 'net_a' reserved range")
    end

    it 'should include the directors ip addresses in the reserved range' do
      ip1 = IPAddr.new('192.168.1.1')
      ip2 = IPAddr.new('192.168.1.2')

      allow(Bosh::Director::Config).to receive(:director_ips).and_return([ip1.to_s, ip2.to_s])
      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'reserved' => ['192.168.0.0', '192.168.0.1', '192.168.0.255'],
          'gateway' => '192.168.0.1',
          'cloud_properties' => { 'foo' => 'bar' },
        },
        [],
      )

      expect(subnet.restricted_ips).to include(ip1)
      expect(subnet.restricted_ips).to include(ip2)
    end

    it 'should only include the directors ip addresses in the reserved range if the ip versions match' do
      ip1 = IPAddr.new('192.168.1.1')
      ip2 = IPAddr.new('2001:db8::2')

      allow(Bosh::Director::Config).to receive(:director_ips).and_return([ip1.to_s, ip2.to_s])
      subnet = make_subnet(
        {
          'range' => '2001:db8::/32',
          'reserved' => [],
          'gateway' => '2001:db8::1',
          'cloud_properties' => { 'foo' => 'bar' },
        },
        [],
      )

      expect(subnet.restricted_ips).to_not include(ip1)
      expect(subnet.restricted_ips).to include(ip2)
    end

    it 'should create a subnet spec with prefix' do
      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'},
          'prefix' => '25'
        },
        [],
      )

      expect(subnet.range.to_s).to eq('192.168.0.0/24')
      expect(subnet.netmask).to eq('255.255.255.0')
      expect(subnet.gateway).to eq('192.168.0.254')
      expect(subnet.prefix).to eq('25')
      expect(subnet.dns).to eq(nil)
    end


    it 'should fail if the prefix size is larger than the range' do
      expect {
        make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'},
          'prefix' => '23'
        },
        [],
      )}.to raise_error(Bosh::Director::NetworkPrefixSizeTooBig,
      "Prefix size '23' is larger than range prefix '24'")
    end

    it 'should ignore static ips which are not a base address of the prefix' do
      ip1 = to_ipaddr('192.168.0.64')
      ip2 = to_ipaddr('192.168.0.128')

      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'static' => ['192.168.0.64','192.168.0.128','192.168.0.191'],
          'cloud_properties' => {'foo' => 'bar'},
          'prefix' => '26'
        },
        [],
      )

      expect(subnet.static_ips).to eq(Set.new([ip1, ip2]))
    end

    it 'should find the correct base address of the prefix from static ip ranges' do
      ip1 = to_ipaddr('192.168.0.32')
      ip2 = to_ipaddr('192.168.0.64')
      ip3 = to_ipaddr('192.168.0.96')

      subnet = make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'static' => ['192.168.0.30-192.168.0.40','192.168.0.96/26'],
          'cloud_properties' => {'foo' => 'bar'},
          'prefix' => '27'
        },
        [],
      )

      expect(subnet.static_ips).to eq(Set.new([ip1, ip2, ip3]))
    end
  end

  describe :overlaps? do
    let(:subnet) do
      make_subnet(
        {
          'range' => '192.168.0.0/24',
          'gateway' => '192.168.0.254',
          'cloud_properties' => {'foo' => 'bar'},
        },
        []
      )
    end

    it 'should return false when the given range does not overlap' do
      other = make_subnet(
        {
          'range' => '192.168.1.0/24',
          'gateway' => '192.168.1.254',
          'cloud_properties' => {'foo' => 'bar'},
        },
        []
      )
      expect(subnet.overlaps?(other)).to eq(false)
    end

    it 'should return true when the given range overlaps' do
      other = make_subnet(
        {
          'range' => '192.168.0.128/28',
          'gateway' => '192.168.0.142',
          'cloud_properties' => {'foo' => 'bar'},
        },
        []
      )
      expect(subnet.overlaps?(other)).to eq(true)
    end

    it 'should return false when IPv4 and IPv6 ranges are compared' do
      other = make_subnet(
        {
          'range' => 'f1ee:0000:0000:0000:0000:0000:0000:0000/64',
          'gateway' => 'f1ee:0000:0000:0000:0000:0000:0000:0001',
          'cloud_properties' => { 'foo' => 'bar' },
        },
        [],
      )
      expect(subnet.overlaps?(other)).to eq(false)
    end
  end

  describe :is_reservable? do
    let(:subnet) do
      make_subnet(
        {
          'range' => '192.168.0.1/24',
          'gateway' => '192.168.0.254',
          'reserved' => reserved
        },
        []
      )
    end
    let(:reserved) { [] }

    context 'when subnet range includes IP' do
      context 'when subnet reserved includes IP' do
        let(:reserved) { ['192.168.0.50-192.168.0.60'] }

        it 'returns false' do
          expect(subnet.is_reservable?(to_ipaddr('192.168.0.55'))).to be_falsey
        end
      end

      context 'when subnet reserved does not include IP' do
        it 'returns true' do
          expect(subnet.is_reservable?(to_ipaddr('192.168.0.55'))).to be_truthy
        end
      end

      context 'when subnet reserved does not include any address of a cidr' do
        let(:reserved) { ['192.168.0.50-192.168.0.60'] }

        it 'returns true' do
          expect(subnet.is_reservable?(to_ipaddr('192.168.0.62/31'))).to be_truthy
        end
      end
    end

    context 'when subnet range does not include IP' do
      it 'returns false' do
        expect(subnet.is_reservable?(to_ipaddr('192.168.10.55'))).to be_falsey
      end
    end

    context 'when subnet range is not the same IP version' do
      it 'returns false' do
        expect(subnet.is_reservable?(to_ipaddr('f1ee:0000:0000:0000:0000:0000:0000:0001'))).to be_falsey
      end
    end
  end
end
