require 'spec_helper'
require 'puppet/provider/aix_object'

describe 'Puppet::Provider::AixObject' do
  let(:resource) do
    Puppet::Type.type(:user).new(
      :name   => 'test_aix_user',
      :ensure => :present
    )
  end
  let(:klass) { Puppet::Provider::AixObject }
  let(:provider) do
    Puppet::Provider::AixObject.new(resource)
  end

  # Clear out the class-level + instance-level mappings
  def clear_attributes
    klass.instance_variable_set(:@mappings, nil)
  end

  before(:each) do
    clear_attributes
  end

  describe '.mapping' do
    let(:puppet_property) { :uid }
    let(:aix_attribute) { :id }
    let(:info) do
      {
        :puppet_property => puppet_property,
        :aix_attribute   => aix_attribute
      }
    end

    shared_examples 'a mapping' do |from, to|
      context "<#{from}> => <#{to}>" do
        let(:from_suffix) { from.to_s.split("_")[-1] }
        let(:to_suffix)   { to.to_s.split("_")[-1] }
        let(:conversion_fn) do
          "convert_#{from_suffix}_value".to_sym
        end

        it 'creates the mapping for a pure conversion function and defines it' do
          conversion_fn_lambda = "#{from_suffix}_to_#{to_suffix}".to_sym
          info[conversion_fn_lambda] = lambda { |x| x.to_s }
          provider.class.mapping(info)
    
          mappings = provider.class.mappings[to]
          expect(mappings).to include(info[from])

          mapping = mappings[info[from]]
          expect(mapping.public_methods).to include(conversion_fn)

          expect(mapping.send(conversion_fn, 3)).to eql('3')
        end

        it 'creates the mapping for an impure conversion function without defining it' do
          conversion_fn_lambda = "#{from_suffix}_to_#{to_suffix}".to_sym
          info[conversion_fn_lambda] = lambda { |provider, x| x.to_s }
          provider.class.mapping(info)
    
          mappings = provider.class.mappings[to]
          expect(mappings).to include(info[from])

          mapping = mappings[info[from]]
          expect(mapping.public_methods).not_to include(conversion_fn)
        end
  
        it 'uses the identity function as the conversion function if none is provided' do
          provider.class.mapping(info)

    
          mappings = provider.class.mappings[to]
          expect(mappings).to include(info[from])

          mapping = mappings[info[from]]
          expect(mapping.public_methods).to include(conversion_fn)

          expect(mapping.send(conversion_fn, 3)).to eql(3)
        end
      end
    end

    include_examples 'a mapping',
                     :puppet_property,
                     :aix_attribute

    include_examples 'a mapping',
                     :aix_attribute,
                     :puppet_property

    it 'sets the AIX attribute to the Puppet property if it is not provided' do
      info[:aix_attribute] = nil
      provider.class.mapping(info)

      mappings = provider.class.mappings[:puppet_property]
      expect(mappings).to include(info[:puppet_property])
    end
  end

  describe '.numeric_mapping' do
    let(:info) do
      info_hash = {
        :puppet_property => :uid,
        :aix_attribute   => :id
      }
      provider.class.numeric_mapping(info_hash)

      info_hash
    end
    let(:aix_attribute) do
      provider.class.mappings[:aix_attribute][info[:puppet_property]]
    end
    let(:puppet_property) do
      provider.class.mappings[:puppet_property][info[:aix_attribute]]
    end

    it 'raises an ArgumentError for a non-numeric Puppet property value' do
      value = 'foo'
      expect do
        aix_attribute.convert_property_value(value) 
      end.to raise_error do |error|
        expect(error).to be_a(ArgumentError)

        expect(error.message).to match(value)
        expect(error.message).to match(info[:puppet_property].to_s)
      end
    end

    it 'converts the numeric Puppet property to a numeric AIX attribute' do
      expect(aix_attribute.convert_property_value(10)).to eql('10')
    end

    it 'converts the numeric AIX attribute to a numeric Puppet property' do
      expect(puppet_property.convert_attribute_value('10')).to eql(10)
    end
  end

  describe '.mk_resource_methods' do
    before(:each) do
      # Add some Puppet properties
      provider.class.mapping(
        puppet_property: :foo,
        aix_attribute: :foo
      )
      provider.class.mapping(
        puppet_property: :bar,
        aix_attribute: :bar
      )

      provider.class.mk_resource_methods
    end

    it 'defines the property getters' do
      provider = Puppet::Provider::AixObject.new(resource)
      provider.instance_variable_set(:@object_info, { :foo => 'foo', :baz => 'baz' })

      (provider.class.mappings[:aix_attribute].keys + [:attributes]).each do |property|
        expect(provider).to receive(:get).with(property).and_return('value')

        expect(provider.send(property)).to eql('value')
      end
    end

    it 'defines the property setters' do
      provider = Puppet::Provider::AixObject.new(resource)

      value = '15'
      provider.class.mappings[:aix_attribute].keys.each do |property|
        expect(provider).to receive(:set).with(property, value)

        provider.send("#{property}=".to_sym, value)
      end
    end
  end

  describe '.parse_colon_separated_list' do
    it 'parses a single empty item' do
      input = ''
      output = ['']

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it 'parses a single nonempty item' do
      input = 'item'
      output = ['item']

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses an escaped ':'" do
      input = '#!:'
      output = [':']

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses a single item with an escaped ':'" do
      input = 'fd8c#!:215d#!:178#!:'
      output = ['fd8c:215d:178:']

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses multiple items that do not have an escaped ':'" do
      input = "foo:bar baz:buu:1234"
      output = ["foo", "bar baz", "buu", "1234"]

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses multiple items some of which have escaped ':'" do
      input = "1234#!:567:foo bar#!:baz:buu#!bob:sally:fd8c#!:215d#!:178"
      output = ["1234:567", "foo bar:baz", "buu#!bob", "sally", 'fd8c:215d:178']

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses a list with several empty items" do
      input = "foo:::bar:baz:boo:"
      output = ["foo", "", "", "bar", "baz", "boo", ""]

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it "parses a list with an escaped ':' and empty item at the end" do
      input = "foo:bar#!::"
      output = ["foo", "bar:", ""]

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end

    it 'parses a real world example' do
      input = File.read(my_fixture('aix_colon_list_real_world_input.out')).chomp
      output = Object.instance_eval(File.read(my_fixture('aix_colon_list_real_world_output.out')))

      expect(provider.class.parse_colon_separated_list(input)).to eql(output)
    end
  end

  describe '.parse_aix_objects' do
    # parse_colon_separated_list is well tested, so we don't need to be
    # as strict on the formatting of the output here. Main point of these
    # tests is to capture the 'wholemeal' parsing that's going on, i.e.
    # that we can parse a bunch of objects together.
    let(:output) do
      <<-AIX_OBJECTS
#name:id:pgrp:groups
root:0:system:system,bin,sys,security,cron,audit,lp
#name:id:pgrp:groups:home:gecos
user:10000:staff:staff:/home/user3:Some User
AIX_OBJECTS
    end

    let(:expected_aix_attributes) do
      [
        {
          :name => 'root',
          :attributes => {
            :id     => '0',
            :pgrp   => 'system',
            :groups => 'system,bin,sys,security,cron,audit,lp',
          }
        },
        {
          :name => 'user',
          :attributes => {
            :id     => '10000',
            :pgrp   => 'staff',
            :groups => 'staff',
            :home   => '/home/user3',
            :gecos  => 'Some User'
          }
        }
      ]
    end

    it 'parses the AIX attributes from the command output' do
      expect(provider.class.parse_aix_objects(output)).to eql(expected_aix_attributes)
    end
  end

  describe 'list_all' do
    let(:output) do
      <<-OUTPUT
#name:id
system:0
#name:id
staff:1
#name:id
bin:2
      OUTPUT
    end

    it 'lists all of the objects' do
      lscmd = 'lsgroups'
      allow(provider.class).to receive(:command).with(:list).and_return(lscmd)
      allow(provider.class).to receive(:execute).with([lscmd, '-c', '-a', 'id', 'ALL']).and_return(output)

      expected_objects = [
        { :name => 'system', :id => '0' },
        { :name => 'staff', :id => '1' },
        { :name => 'bin', :id => '2' }
      ]
      expect(provider.class.list_all).to eql(expected_objects)
    end
  end

  describe '.instances' do
    let(:objects) do
      [
        { :name => 'group1', :id => '1' },
        { :name => 'group2', :id => '2' }
      ]
    end

    it 'returns all of the available instances' do
      allow(provider.class).to receive(:list_all).and_return(objects)

      expect(provider.class.instances.map(&:name)).to eql(['group1', 'group2'])
    end
  end

  describe '#mappings' do
    # Returns a pair [ instance_level_mapped_object, class_level_mapped_object ]
    def mapped_objects(type, input)
      [
        provider.mappings[type][input],
        provider.class.mappings[type][input]
      ]
    end

    before(:each) do
      # Create a pure mapping
      provider.class.numeric_mapping(
        puppet_property: :pure_puppet_property,
        aix_attribute: :pure_aix_attribute
      )

      # Create an impure mapping
      impure_conversion_fn = lambda do |provider, value|
        "Provider instance's name is #{provider.name}"
      end
      provider.class.mapping(
        puppet_property: :impure_puppet_property,
        aix_attribute: :impure_aix_attribute,
        property_to_attribute: impure_conversion_fn,
        attribute_to_property: impure_conversion_fn
      )
    end

    it 'memoizes the result' do
      provider.instance_variable_set(:@mappings, 'memoized')
      expect(provider.mappings).to eql('memoized')
    end

    it 'creates the instance-level mappings with the same structure as the class-level one' do
      expect(provider.mappings.keys).to eql(provider.class.mappings.keys)
      provider.mappings.keys.each do |type|
        expect(provider.mappings[type].keys).to eql(provider.class.mappings[type].keys)
      end
    end
    
    shared_examples 'uses the right mapped object for a given mapping' do |from_type, to_type|
      context "<#{from_type}> => <#{to_type}>" do
        it 'shares the class-level mapped object for pure mappings' do
          input = "pure_#{from_type}".to_sym

          instance_level_mapped_object, class_level_mapped_object = mapped_objects(to_type, input)
          expect(instance_level_mapped_object.object_id).to eql(class_level_mapped_object.object_id)
        end

        it 'dups the class-level mapped object for impure mappings' do
          input = "impure_#{from_type}".to_sym

          instance_level_mapped_object, class_level_mapped_object = mapped_objects(to_type, input)
          expect(instance_level_mapped_object.object_id).to_not eql(
            class_level_mapped_object.object_id
          )
        end

        it 'defines the conversion function for impure mappings' do
          from_type_suffix = from_type.to_s.split("_")[-1]
          conversion_fn = "convert_#{from_type_suffix}_value".to_sym

          input = "impure_#{from_type}".to_sym
          mapped_object, _ = mapped_objects(to_type, input)

          expect(mapped_object.public_methods).to include(conversion_fn)
          expect(mapped_object.send(conversion_fn, 3)).to match(provider.name)
        end
      end
    end

    include_examples 'uses the right mapped object for a given mapping',
                     :puppet_property,
                     :aix_attribute

    include_examples 'uses the right mapped object for a given mapping',
                     :aix_attribute,
                     :puppet_property
  end

  describe '#attributes_to_args' do
    let(:attributes) do
      {
        :attribute1 => 'value1',
        :attribute2 => 'value2'
      }
    end

    it 'converts the attributes hash to CLI arguments' do
      expect(provider.attributes_to_args(attributes)).to eql(
        ["attribute1=value1", "attribute2=value2"]
      )
    end
  end

  describe '#ia_module_args' do
    it 'returns no arguments if ia_load_module parameter or forcelocal parameter are not specified' do
      allow(provider.resource).to receive(:[]).with(:ia_load_module).and_return(nil)
      allow(provider.resource).to receive(:[]).with(:forcelocal).and_return(nil)
      expect(provider.ia_module_args).to eql([])
    end

    it 'returns the ia_load_module as a CLI argument when ia_load_module is specified' do
      allow(provider.resource).to receive(:[]).with(:ia_load_module).and_return('module')
      allow(provider.resource).to receive(:[]).with(:forcelocal).and_return(nil)
      expect(provider.ia_module_args).to eql(['-R', 'module'])
    end

    it 'returns "files" as a CLI argument when forcelocal is specified' do
      allow(provider.resource).to receive(:[]).with(:ia_load_module).and_return(nil)
      allow(provider.resource).to receive(:[]).with(:forcelocal).and_return(true)
      expect(provider.ia_module_args).to eql(['-R', 'files'])
    end

    it 'raises argument error when both ia_load_module and forcelocal parameters are set' do
      allow(provider.resource).to receive(:[]).with(:ia_load_module).and_return('files')
      allow(provider.resource).to receive(:[]).with(:forcelocal).and_return(true)
      expect { provider.ia_module_args }.to raise_error(ArgumentError, "Cannot have both 'forcelocal' and 'ia_load_module' at the same time!")
    end
  end

  describe '#lscmd' do
    it 'returns the lscmd' do
      allow(provider.class).to receive(:command).with(:list).and_return('list')
      allow(provider).to receive(:ia_module_args).and_return(['ia_module_args'])

      expect(provider.lscmd).to eql(
        ['list', '-c', 'ia_module_args', provider.resource.name]
      )
    end
  end

  describe '#addcmd' do
    let(:attributes) do
      {
        :attribute1 => 'value1',
        :attribute2 => 'value2'
      }
    end

    it 'returns the addcmd passing in the attributes as CLI arguments' do
      allow(provider.class).to receive(:command).with(:add).and_return('add')
      allow(provider).to receive(:ia_module_args).and_return(['ia_module_args'])

      expect(provider.addcmd(attributes)).to eql(
        ['add', 'ia_module_args', 'attribute1=value1', 'attribute2=value2', provider.resource.name]
      )
    end
  end

  describe '#deletecmd' do
    it 'returns the lscmd' do
      allow(provider.class).to receive(:command).with(:delete).and_return('delete')
      allow(provider).to receive(:ia_module_args).and_return(['ia_module_args'])

      expect(provider.deletecmd).to eql(
        ['delete', 'ia_module_args', provider.resource.name]
      )
    end
  end

  describe '#modifycmd' do
    let(:attributes) do
      {
        :attribute1 => 'value1',
        :attribute2 => 'value2'
      }
    end

    it 'returns the addcmd passing in the attributes as CLI arguments' do
      allow(provider.class).to receive(:command).with(:modify).and_return('modify')
      allow(provider).to receive(:ia_module_args).and_return(['ia_module_args'])

      expect(provider.modifycmd(attributes)).to eql(
        ['modify', 'ia_module_args', 'attribute1=value1', 'attribute2=value2', provider.resource.name]
      )
    end
  end

  describe '#modify_object' do
    let(:new_attributes) do
      {
        :nofiles => 10000,
        :fsize   => 30000
      }
    end

    it 'modifies the AIX object with the new attributes' do
      allow(provider).to receive(:modifycmd).with(new_attributes).and_return('modify_cmd')
      expect(provider).to receive(:execute).with('modify_cmd')
      expect(provider).to receive(:object_info).with(true)

      provider.modify_object(new_attributes)
    end
  end

  describe '#get' do
    # Input
    let(:property) { :uid }
    
    let!(:object_info) do
      hash = {}

      provider.instance_variable_set(:@object_info, hash)
      hash
    end

    it 'returns :absent if the AIX object does not exist' do
      allow(provider).to receive(:exists?).and_return(false)
      object_info[property] = 15
      
      expect(provider.get(property)).to eql(:absent)
    end

    it 'returns :absent if the property is not present on the system' do
      allow(provider).to receive(:exists?).and_return(true)

      expect(provider.get(property)).to eql(:absent)
    end

    it "returns the property's value" do
      allow(provider).to receive(:exists?).and_return(true)
      object_info[property] = 15

      expect(provider.get(property)).to eql(15)
    end
  end

  describe '#set' do
    # Input
    let(:property) { :uid }
    let(:value) { 10 }

    # AIX attribute params
    let(:aix_attribute) { :id }
    let(:property_to_attribute) do
      lambda { |x| x.to_s }
    end

    before(:each) do
      # Add an attribute
      provider.class.mapping(
        puppet_property: property,
        aix_attribute: aix_attribute,
        property_to_attribute: property_to_attribute
      )
    end

    it "raises a Puppet::Error if it fails to set the property's value" do
      allow(provider).to receive(:modify_object)
        .with({ :id => value.to_s })
        .and_raise(Puppet::ExecutionFailure, 'failed to modify the AIX object!')

      expect { provider.set(property, value) }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)
      end
    end

    it "sets the given property's value to the passed-in value" do
      expect(provider).to receive(:modify_object).with({ :id => value.to_s })

      provider.set(property, value)
    end
  end

  describe '#validate_new_attributes' do
    let(:new_attributes) do
      {
        :nofiles => 10000,
        :fsize   => 100000
      }
    end

    it 'raises a Puppet::Error if a specified attributes corresponds to a Puppet property, reporting all of the attribute-property conflicts' do
      provider.class.mapping(puppet_property: :uid, aix_attribute: :id)
      provider.class.mapping(puppet_property: :groups, aix_attribute: :groups)

      new_attributes[:id] = '25'
      new_attributes[:groups] = 'groups'

      expect { provider.validate_new_attributes(new_attributes) }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match("'uid', 'groups'")
        expect(error.message).to match("'id', 'groups'")
      end
    end
  end

  describe '#attributes=' do
    let(:new_attributes) do
      {
        :nofiles => 10000,
        :fsize   => 100000
      }
    end

    it 'raises a Puppet::Error if one of the specified attributes corresponds to a Puppet property' do
      provider.class.mapping(puppet_property: :uid, aix_attribute: :id)
      new_attributes[:id] = '25'

      expect { provider.attributes = new_attributes }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match('uid')
        expect(error.message).to match('id')
      end
    end

    it 'raises a Puppet::Error if it fails to set the new AIX attributes' do
      allow(provider).to receive(:modify_object)
        .with(new_attributes)
        .and_raise(Puppet::ExecutionFailure, 'failed to modify the AIX object!')
      
      expect { provider.attributes = new_attributes }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match('failed to modify the AIX object!')
      end
    end

    it 'sets the new AIX attributes' do
      expect(provider).to receive(:modify_object).with(new_attributes)

      provider.attributes = new_attributes
    end
  end

  describe '#object_info' do
    before(:each) do
      # Add some Puppet properties
      provider.class.mapping(
        puppet_property: :uid,
        aix_attribute: :id,
        attribute_to_property: lambda { |x| x.to_i },
      )
      provider.class.mapping(
        puppet_property: :groups,
        aix_attribute: :groups
      )

      # Mock out our lscmd
      allow(provider).to receive(:lscmd).and_return("lsuser #{resource[:name]}")
    end

    it 'memoizes the result' do
      provider.instance_variable_set(:@object_info, {})
      expect(provider.object_info).to eql({})
    end

    it 'returns nil if the AIX object does not exist' do
      allow(provider).to receive(:execute).with(provider.lscmd).and_raise(
        Puppet::ExecutionFailure, 'lscmd failed!'
      )

      expect(provider.object_info).to be_nil
    end

    it 'collects the Puppet properties' do
      output = 'mock_output'
      allow(provider).to receive(:execute).with(provider.lscmd).and_return(output)

      # Mock the AIX attributes on the system
      mock_attributes = {
        :id         => '1',
        :groups     => 'foo,bar,baz',
        :attribute1 => 'value1',
        :attribute2 => 'value2'
      }
      allow(provider.class).to receive(:parse_aix_objects)
        .with(output)
        .and_return([{ :name => resource.name, :attributes => mock_attributes }])

      expected_property_values = {
        :uid        => 1,
        :groups     => 'foo,bar,baz',
        :attributes => {
          :attribute1 => 'value1',
          :attribute2 => 'value2'
        }
      }
      provider.object_info
      expect(provider.instance_variable_get(:@object_info)).to eql(expected_property_values)
    end
  end

  describe '#exists?' do
    it 'should return true if the AIX object exists' do
      allow(provider).to receive(:object_info).and_return({})
      expect(provider.exists?).to be(true)
    end

    it 'should return false if the AIX object does not exist' do
      allow(provider).to receive(:object_info).and_return(nil)
      expect(provider.exists?).to be(false)
    end
  end

  describe "#create" do
    let(:property_attributes) do
      {}
    end

    def stub_attributes_property(attributes)
      allow(provider.resource).to receive(:should).with(:attributes).and_return(attributes)
    end

    def set_property(puppet_property, aix_attribute, property_to_attribute, should_value = nil)
      property_to_attribute ||= lambda { |x| x }

      provider.class.mapping(
        puppet_property: puppet_property,
        aix_attribute: aix_attribute,
        property_to_attribute: property_to_attribute
      )
      allow(provider.resource).to receive(:should).with(puppet_property).and_return(should_value)

      if should_value
        property_attributes[aix_attribute] = property_to_attribute.call(should_value)
      end
    end

    before(:each) do
      clear_attributes
      
      # Clear out the :attributes property. We will be setting this later.
      stub_attributes_property(nil)

      # Add some properties
      set_property(:uid, :id, lambda { |x| x.to_s }, 10)
      set_property(:groups, :groups, nil, 'group1,group2,group3')
      set_property(:shell, :shell, nil)
    end

    it 'raises a Puppet::Error if one of the specified attributes corresponds to a Puppet property' do
      stub_attributes_property({ :id => 15 })
      provider.class.mapping(puppet_property: :uid, aix_attribute: :id)

      expect { provider.create }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match('uid')
        expect(error.message).to match('id')
      end
    end

    it "raises a Puppet::Error if it fails to create the AIX object" do
      allow(provider).to receive(:addcmd)
      allow(provider).to receive(:execute).and_raise(
        Puppet::ExecutionFailure, "addcmd failed!"
      )

      expect { provider.create }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match("not create")
      end
    end

    it "creates the AIX object with the given AIX attributes + Puppet properties" do
      attributes = { :fsize => 1000 }
      stub_attributes_property(attributes)

      expect(provider).to receive(:addcmd)
        .with(attributes.merge(property_attributes))
        .and_return('addcmd')
      expect(provider).to receive(:execute).with('addcmd')

      provider.create
    end
  end
 
  describe "#delete" do
    before(:each) do
      allow(provider).to receive(:deletecmd).and_return('deletecmd')
    end

    it "raises a Puppet::Error if it fails to delete the AIX object" do
      allow(provider).to receive(:execute).with(provider.deletecmd).and_raise(
        Puppet::ExecutionFailure, "deletecmd failed!"
      )

      expect { provider.delete }.to raise_error do |error|
        expect(error).to be_a(Puppet::Error)

        expect(error.message).to match("not delete")
      end
    end

    it "deletes the AIX object" do
      expect(provider).to receive(:execute).with(provider.deletecmd)
      expect(provider).to receive(:object_info).with(true)

      provider.delete
    end
  end
end
