require 'spec_helper'

describe Keychain do

  describe 'user interaction' do
    it 'should be true by default' do
      expect(Keychain.user_interaction_allowed?).to be_truthy
    end

    it 'should be changeable' do
      Keychain.user_interaction_allowed = false
      expect(Keychain.user_interaction_allowed?).to be_falsey
      Keychain.user_interaction_allowed = true
      expect(Keychain.user_interaction_allowed?).to be_truthy
    end
  end

  describe 'default' do
    it "should return the login keychain" do
      expect(Keychain.default.path).to eq(File.expand_path(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain-db')))
    end
  end

  describe 'open' do
    it 'should create a keychain reference to a path' do
      keychain = Keychain.open(File.join(ENV['HOME'], 'Library','Keychains', 'login.keychain'))
      expect(keychain.path).to eq(Keychain.default.path)
    end

    it 'should raise when passed a nil path' do
      expect {Keychain.open(nil)}.to raise_error(ArgumentError)
    end
  end

  describe 'create' do
    it 'should create the keychain' do
      begin
        keychain = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"),
                            'password');
        expect(File.exists?(keychain.path)).to be_truthy
      ensure
       keychain.delete
      end
    end

    context 'no password supplied' do
      #we have to stub this out as it would trigger a dialog box prompting for a password
      let(:result) do
        instance_double(Keychain::Keychain)
      end

      it 'should create a keychain by prompting the user' do
        #we can't just use a kind_of matcher becaue FFI::Pointer#== raises an exception
        #when compared to non pointer values
        mock_pointer = double(FFI::MemoryPointer, :read_pointer => 123456)
        allow(FFI::MemoryPointer).to receive(:new).with(:pointer).and_return(mock_pointer)

        expect(Sec).to receive('SecKeychainCreate').with('akeychain', 0, nil, 1, nil,mock_pointer).and_return(0)

        expect(Keychain::Keychain).to receive(:new).with(mock_pointer.read_pointer).and_return(result)
        expect(result).to receive(:release_on_gc).and_return(result)
        expect(Keychain.create('akeychain')).to eq(result)
      end
    end
  end

  describe 'import' do
    before(:all) do
      @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
      @rsa_key = OpenSSL::PKey::RSA.new(2048).to_s
    end

    it 'should import item to the keychain' do
      imported_key = @keychain.import(@rsa_key, ['/usr/bin/codesign']).first
      imported_key.load_attributes
      found_key = Keychain::Scope.new(Sec::Classes::KEY, @keychain).all.first
      expect(imported_key.attributes).to eq(found_key.attributes)
    end

    it 'should raise an exception for duplicated item' do
      expect { @keychain.import(@rsa_key) }.to raise_error(Keychain::DuplicateItemError)
    end

    after(:all) do
      @keychain.delete
    end
  end

  describe 'exists?' do
    context 'the keychain exists' do
      it 'should return true' do
        expect(Keychain.default.exists?).to be_truthy
      end
    end 

    context 'the keychain does not exist' do
      it 'should return false' do
        k = Keychain.open('/some/path/that/does/not/exist')
        expect(k.exists?).to be_falsey
      end
    end
  end

  describe 'settings' do
    before(:all) do
      @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
    end

    it 'should read/write lock_on_sleep' do
      @keychain.lock_on_sleep = true
      expect(@keychain.lock_on_sleep?).to eq(true)
      @keychain.lock_on_sleep = false
      expect(@keychain.lock_on_sleep?).to eq(false)
    end

    it 'should read/write lock_interval' do
      @keychain.lock_interval = 12345
      expect(@keychain.lock_interval).to eq(12345)
    end

    after(:all) do
      @keychain.delete
    end
  end

  describe 'locking' do
    context 'with a locked keychain' do
      before :each do
        @keychain = Keychain.create(File.join(Dir.tmpdir, "keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass')
        @keychain.lock!
      end

      it 'should raise on invalid password' do
        expect {@keychain.unlock! 'badpassword'}.to raise_error(Keychain::AuthFailedError)
      end

      it 'should unlock on valid password' do
        @keychain.unlock! 'pass'
        expect(@keychain).not_to be_locked
      end
    end
  end

  shared_examples_for 'item collection' do

    before(:each) do
      @keychain_1 = Keychain.create(File.join(Dir.tmpdir, "other_keychain_spec_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass').add_to_search_list
      @keychain_2 = Keychain.create(File.join(Dir.tmpdir, "keychain_2_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass').add_to_search_list
      @keychain_3 = Keychain.create(File.join(Dir.tmpdir, "keychain_3_#{Time.now.to_i}_#{Time.now.usec}_#{rand(1000)}.keychain"), 'pass').add_to_search_list
      add_fixtures
    end

    after(:each) do
      @keychain_1.delete
      @keychain_2.delete
      @keychain_3.delete
    end
    
    describe('create') do
      it 'should add a password' do
        item =  @keychain_1.send(subject).create(create_arguments)
        expect(item).to be_a(Keychain::Item)
        expect(item.klass).to eq(expected_kind)
        expect(item.password).to eq('some-password')
      end

      it 'should be findable' do        
        @keychain_1.send(subject).create(create_arguments)
        item = @keychain_1.send(subject).where(search_for_created_arguments).first
        expect(item.password).to eq('some-password')
      end

      context 'when a duplicate item exists' do
        before(:each) do
          @keychain_1.send(subject).create(create_arguments)
        end

        it 'should raise Keychain::DuplicateItemError' do
          expect {@keychain_1.send(subject).create(create_arguments)}.to raise_error(Keychain::DuplicateItemError)
        end
      end
    end

    describe('all') do

      context 'when the keychain does not contains a matching item' do
        it 'should return []' do
          expect(@keychain_1.send(subject).where(search_arguments_with_no_results).all).to eq([])
        end
      end

      it 'should return an array of results' do
        item = @keychain_1.send(subject).where(search_arguments).all.first
        expect(item).to be_a(Keychain::Item)
        expect(item.password).to eq('some-password-1')
      end

      context 'searching all keychains' do
        context 'when the keychain does contains matching items' do
          it 'should return all of them' do
            expect(Keychain.send(subject).where(search_arguments_with_multiple_results).all.length).to eq(3)
          end
        end

        context 'when the limit is option is set' do
          it 'should limit the return set' do
            expect(Keychain.send(subject).where(search_arguments_with_multiple_results).limit(1).all.length).to eq(1)
          end
        end

        context 'when a subset of keychains is specified' do
          it 'should return items from those keychains' do
            expect(Keychain.send(subject).where(search_arguments_with_multiple_results).in(@keychain_1, @keychain_2).all.length).to eq(2)
          end
        end
      end
    end
    describe 'first' do
      context 'when the keychain does not contain a matching item' do
        it 'should return nil' do
          item = expect(@keychain_1.send(subject).where(search_arguments_with_no_results).first).to be_nil
        end
      end

      context 'when the keychain does contain a matching item' do
        it 'should find it' do
          item = @keychain_1.send(subject).where(search_arguments).first
          expect(item).to be_a(Keychain::Item)
          expect(item.password).to eq('some-password-1')
        end
      end

      context 'when a different keychain contains a matching item' do
        before(:each) do
          item = @keychain_1.send(subject).create(create_arguments)
        end

        it 'should not find it' do
          expect(@keychain_2.send(subject).where(search_arguments).first).to be_nil
        end
      end
    end
  end

  describe 'generic_passwords' do
    subject { :generic_passwords }
    let(:create_arguments){{:service => 'aservice', :account => 'anaccount-foo', :password =>'some-password'}}
    let(:search_for_created_arguments){{:service => 'aservice'}}

    let(:search_arguments){{:service => 'aservice-1'}}
    let(:search_arguments_with_no_results){{:service => 'doesntexist'}}
    let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
    let(:expected_kind) {'genp'}

    def add_fixtures
      @keychain_1.generic_passwords.create(:service => 'aservice-1', :account => 'anaccount', :password => 'some-password-1')
      @keychain_2.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-2')
      @keychain_3.generic_passwords.create(:service => 'aservice-2', :account => 'anaccount', :password => 'some-password-3')
    end
    it_behaves_like 'item collection'
  end

  describe 'internet_passwords' do
    subject { :internet_passwords }
    let(:create_arguments){{:server => 'dressipi.example.com', :account => 'anaccount-foo', :password =>'some-password', :protocol => Keychain::Protocols::HTTP}}
    let(:search_for_created_arguments){{:server => 'dressipi.example.com', :protocol => Keychain::Protocols::HTTP}}
    let(:search_arguments){{:server => 'dressipi-1.example.com', :protocol => Keychain::Protocols::HTTP}}
    let(:search_arguments_with_no_results){{:server => 'dressipi.example.com'}}
    let(:search_arguments_with_multiple_results){{:account => 'anaccount'}}
    let(:expected_kind) {'inet'}

    def add_fixtures
      @keychain_1.internet_passwords.create(:server => 'dressipi-1.example.com', :account => 'anaccount', :password => 'some-password-1', :protocol => Keychain::Protocols::HTTP)
      @keychain_2.internet_passwords.create(:server => 'dressipi-2.example.com', :account => 'anaccount', :password => 'some-password-2', :protocol => Keychain::Protocols::HTTP)
      @keychain_3.internet_passwords.create(:server => 'dressipi-3.example.com', :account => 'anaccount', :password => 'some-password-3', :protocol => Keychain::Protocols::HTTP)
    end
    it_behaves_like 'item collection'
  end
end