# encoding: utf-8
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0


require "spec_helper"

describe Vault::Rails do
  before(:all) do
    Vault::Rails.sys.mount("transit", :transit)
  end

  context "with default options" do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_ssn")
    end

    it "encrypts attributes" do
      person = Person.create!(ssn: "123-45-6789")
      expect(person.ssn_encrypted).to be
      expect(person.ssn_encrypted.encoding).to eq(Encoding::UTF_8)
    end

    it "decrypts attributes" do
      person = Person.create!(ssn: "123-45-6789")
      person.reload

      expect(person.ssn).to eq("123-45-6789")
      expect(person.ssn.encoding).to eq(Encoding::UTF_8)
    end

    it "tracks dirty attributes" do
      person = Person.create!(ssn: "123-45-6789")

      expect(person.ssn_changed?).to be(false)
      expect(person.ssn_change).to be(nil)
      expect(person.ssn_was).to eq("123-45-6789")

      person.ssn = "111-11-1111"

      expect(person.ssn_changed?).to be(true)
      expect(person.ssn_change).to eq(["123-45-6789", "111-11-1111"])
      expect(person.ssn_was).to eq("123-45-6789")
    end

    it "allows attributes to be unset" do
      person = Person.create!(ssn: "123-45-6789")
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows dirty attributes to be unset" do
      person = Person.create!(ssn: "123-45-6789")
      person.ssn = nil
      expect(person.ssn).to be_nil

      person2 = Person.create!(ssn: "123-45-6789")
      person2.assign_attributes(ssn: nil)
      expect(person2.ssn).to be_nil
    end

    it "allows saving without validations" do
      person = Person.new(ssn: "123-456-7890")
      person.save(validate: false)
      expect(person.ssn_encrypted).to match("vault:")
    end

    it "allows attributes to be unset after reload" do
      person = Person.create!(ssn: "123-45-6789")
      person.reload
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows attributes to be blank" do
      person = Person.create!(ssn: "123-45-6789")
      person.update!(ssn: "")
      person.reload

      expect(person.ssn).to eq("")
      expect(person.ssn_encrypted).to eq("")
    end

    it "allows attributes to be null" do
      person = Person.create!(ssn: "123-45-6789")
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to eq(nil)
      expect(person.ssn_encrypted).to eq(nil)
    end

    it "reloads instance variables on reload" do
      person = Person.create!(ssn: "123-45-6789")
      expect(person.instance_variable_get(:@ssn)).to eq("123-45-6789")

      person.ssn = "111-11-1111"
      person.reload
      expect(person.instance_variable_get(:@ssn)).to eq("123-45-6789")
    end

    it "does not try to encrypt unchanged attributes" do
      person = Person.create!(ssn: "123-45-6789")

      expect(Vault::Rails).to_not receive(:encrypt)
      person.name = "Cinderella"
      person.save!
    end

    it "does not register a Vault attribute as necessarily being backed by a column" do
      expect(Person.attribute_names).to include("ssn")
      expect(Person.column_names).not_to include("ssn")
    end

    it "does not reload encrypted attributes on destroy" do
      person = Person.create!(ssn: "123-45-6789")

      expect(Vault::Rails).to_not receive(:decrypt)
      person.destroy
    end
  end

  context "lazy decrypt" do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_ssn")
    end

    it "encrypts attributes" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      expect(person.ssn_encrypted).to be
      expect(person.ssn_encrypted.encoding).to eq(Encoding::UTF_8)
    end

    it "decrypts attributes" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.reload

      expect(person.ssn).to eq("123-45-6789")
      expect(person.ssn.encoding).to eq(Encoding::UTF_8)
    end

    it "does not decrypt on initialization" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.reload

      p2 = LazyPerson.find(person.id)

      expect(p2.instance_variable_get("@ssn")).to eq(nil)
      expect(p2.ssn).to eq("123-45-6789")
    end

    it "tracks dirty attributes" do
      person = LazyPerson.create!(ssn: "123-45-6789")

      expect(person.ssn_changed?).to be(false)
      expect(person.ssn_change).to be(nil)
      expect(person.ssn_was).to eq("123-45-6789")

      person.ssn = "111-11-1111"

      expect(person.ssn_changed?).to be(true)
      expect(person.ssn_change).to eq(["123-45-6789", "111-11-1111"])
      expect(person.ssn_was).to eq("123-45-6789")

      person.assign_attributes(ssn: "222-22-2222")

      expect(person.ssn_changed?).to be(true)
      expect(person.ssn_change).to eq(["123-45-6789", "222-22-2222"])
      expect(person.ssn_was).to eq("123-45-6789")
    end

    it "allows attributes to be unset" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows dirty attributes to be unset" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.ssn = nil
      expect(person.ssn).to be_nil

      person2 = LazyPerson.create!(ssn: "123-45-6789")
      person2.assign_attributes(ssn: nil)
      expect(person2.ssn).to be_nil
    end


    it "allows saving without validations" do
      person = LazyPerson.new(ssn: "123-456-7890")
      expect(person.save(validate: false)).to be(true)
      expect(person.ssn_encrypted).to match("vault:")
    end

    it "allows attributes to be unset after reload" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.reload
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows attributes to be blank" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      person.update!(ssn: "")
      person.reload

      expect(person.ssn).to eq("")
    end

    it "reloads instance variables on reload" do
      person = LazyPerson.create!(ssn: "123-45-6789")
      expect(person.instance_variable_get(:@ssn)).to eq("123-45-6789")

      person.ssn = "111-11-1111"
      person.reload

      expect(person.ssn).to eq("123-45-6789")
    end

    it "does not try to encrypt unchanged attributes" do
      person = LazyPerson.create!(ssn: "123-45-6789")

      expect(Vault::Rails).to_not receive(:encrypt)
      person.name = "Cinderella"
      person.save!
    end

    it "allows attributes to be accessed after a destroy" do
      person = LazyPerson.create!(ssn: "123-45-6789")

      person.destroy
      expect { person.ssn }.not_to raise_error
    end
  end

  context "lazy single decrypt" do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_ssn")
    end

    it "encrypts attributes" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      expect(person.ssn_encrypted.length).to eq(61)
      expect(person.ssn_encrypted).to start_with("vault:v1:")
      expect(person.ssn_encrypted.encoding).to eq(Encoding::UTF_8)
    end

    it "decrypts attributes" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.reload

      expect(person.ssn).to eq("123-45-6789")
      expect(person.ssn.encoding).to eq(Encoding::UTF_8)
    end

    it "does not decrypt on initialization" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.reload

      p2 = LazySinglePerson.find(person.id)

      expect(p2.instance_variable_get("@ssn")).to eq(nil)
      expect(p2.ssn).to eq("123-45-6789")
    end

    it "does not decrypt all attributes on single read" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.update!(credit_card: "abcd-efgh-hijk-lmno")
      expect(person.credit_card).to eq("abcd-efgh-hijk-lmno")

      person.reload

      p2 = LazySinglePerson.find(person.id)

      expect(p2.instance_variable_get("@ssn")).to eq(nil)
      expect(p2.ssn).to eq("123-45-6789")
      expect(p2.instance_variable_get("@credit_card")).to eq(nil)
      expect(p2.credit_card).to eq("abcd-efgh-hijk-lmno")
    end

    it "does not decrypt all attributes on single write" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.update!(credit_card: "abcd-efgh-hijk-lmno")
      expect(person.credit_card).to eq("abcd-efgh-hijk-lmno")

      person.reload

      p2 = LazySinglePerson.find(person.id)

      expect(p2.instance_variable_get("@ssn")).to eq(nil)
      expect(p2.ssn).to eq("123-45-6789")
      person.ssn = "111-11-1111"
      expect(p2.instance_variable_get("@credit_card")).to eq(nil)
      expect(p2.credit_card).to eq("abcd-efgh-hijk-lmno")
    end

    it "tracks dirty attributes" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")

      expect(person.ssn_changed?).to be(false)
      expect(person.ssn_change).to be(nil)
      expect(person.ssn_was).to eq("123-45-6789")

      person.ssn = "111-11-1111"

      expect(person.ssn_changed?).to be(true)
      expect(person.ssn_change).to eq(["123-45-6789", "111-11-1111"])
      expect(person.ssn_was).to eq("123-45-6789")
    end

    it "allows attributes to be unset" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows saving without validations" do
      person = LazySinglePerson.new(ssn: "123-456-7890")
      expect(person.save(validate: false)).to be(true)
      expect(person.ssn_encrypted).to match("vault:")
    end

    it "allows attributes to be unset after reload" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.reload
      person.update!(ssn: nil)
      person.reload

      expect(person.ssn).to be(nil)
    end

    it "allows attributes to be blank" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      person.update!(ssn: "")
      person.reload

      expect(person.ssn).to eq("")
    end

    it "reloads instance variables on reload" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")
      expect(person.instance_variable_get(:@ssn)).to eq("123-45-6789")

      person.ssn = "111-11-1111"
      person.reload

      expect(person.ssn).to eq("123-45-6789")
    end

    it "does not try to encrypt unchanged attributes" do
      person = LazySinglePerson.create!(ssn: "123-45-6789")

      expect(Vault::Rails).to_not receive(:encrypt)
      person.name = "Cinderella"
      person.save!
    end

    it "allows attributes to be accessed after a destroy" do
      person = LazyPerson.create!(ssn: "123-45-6789")

      person.destroy
      expect { person.ssn }.not_to raise_error
    end
  end

  context "with custom options" do
    before(:all) do
      Vault::Rails.sys.mount("credit-secrets", :transit)
      Vault::Rails.logical.write("credit-secrets/keys/people_credit_cards")
    end

    it "encrypts attributes" do
      person = Person.create!(credit_card: "1234567890111213")
      expect(person.cc_encrypted).to be
      expect(person.cc_encrypted.encoding).to eq(Encoding::UTF_8)
    end

    it "decrypts attributes" do
      person = Person.create!(credit_card: "1234567890111213")
      person.reload

      expect(person.credit_card).to eq("1234567890111213")
      expect(person.credit_card.encoding).to eq(Encoding::UTF_8)
    end

    it "tracks dirty attributes" do
      person = Person.create!(credit_card: "1234567890111213")

      expect(person.credit_card_changed?).to be(false)
      expect(person.credit_card_change).to eq(nil)
      expect(person.credit_card_was).to eq("1234567890111213")

      person.credit_card = "123456789010"

      expect(person.credit_card_changed?).to be(true)
      expect(person.credit_card_change).to eq(["1234567890111213", "123456789010"])
      expect(person.credit_card_was).to eq("1234567890111213")
    end

    it "allows attributes to be unset" do
      person = Person.create!(credit_card: "1234567890111213")
      person.update!(credit_card: nil)
      person.reload

      expect(person.credit_card).to be(nil)
    end

    it "allows attributes to be blank" do
      person = Person.create!(credit_card: "1234567890111213")
      person.update!(credit_card: "")
      person.reload

      expect(person.credit_card).to eq("")
    end
  end

  context "with non-ASCII characters" do
    before(:all) do
      Vault::Rails.sys.mount("non-ascii", :transit)
      Vault::Rails.logical.write("non-ascii/keys/people_non_ascii")
    end

    it "encrypts attributes" do
      person = Person.create!(non_ascii: "dás ümlaut")
      expect(person.non_ascii_encrypted).to be
      expect(person.non_ascii_encrypted.encoding).to eq(Encoding::UTF_8)
    end

    it "decrypts attributes" do
      person = Person.create!(non_ascii: "dás ümlaut")
      person.reload

      expect(person.non_ascii).to eq("dás ümlaut")
      expect(person.non_ascii.encoding).to eq(Encoding::UTF_8)
    end

    it "tracks dirty attributes" do
      person = Person.create!(non_ascii: "dás ümlaut")

      expect(person.non_ascii_changed?).to be(false)
      expect(person.non_ascii_change).to eq(nil)
      expect(person.non_ascii_was).to eq("dás ümlaut")

      person.non_ascii = "él ñiñô"

      expect(person.non_ascii_changed?).to be(true)
      expect(person.non_ascii_change).to eq(["dás ümlaut", "él ñiñô"])
      expect(person.non_ascii_was).to eq("dás ümlaut")
    end

    it "allows attributes to be unset" do
      person = Person.create!(non_ascii: "dás ümlaut")
      person.update!(non_ascii: nil)
      person.reload

      expect(person.non_ascii).to be(nil)
    end

    it "allows attributes to be blank" do
      person = Person.create!(non_ascii: "dás ümlaut")
      person.update!(non_ascii: "")
      person.reload

      expect(person.non_ascii).to eq("")
    end
  end

  context "with a default" do
    %i[new create].each do |creation_method|
      context "on #{creation_method}" do
        context "without an initial attribute" do
          it "sets the default" do
            person = Person.public_send(creation_method)
            expect(person.default).to eq("abc123")
            person.save!
            person.reload
            expect(person.default).to eq("abc123")
          end
        end

        context "with an initial attribute" do
          it "does not set the default" do
            person = Person.public_send(creation_method, default: "another")
            expect(person.default).to eq("another")
            person.save!
            person.reload
            expect(person.default).to eq("another")
          end
        end
      end
    end
  end

  context "with a default and serializer" do
    %i[new create].each do |creation_method|
      context "on #{creation_method}" do
        context "without an initial attribute" do
          it "sets the default" do
            person = Person.public_send(creation_method)
            expect(person.default_with_serializer).to eq({})
            person.save!
            person.reload
            expect(person.default_with_serializer).to eq({})
          end
        end

        context "with an initial attribute" do
          it "does not set the default" do
            person = Person.public_send(
              creation_method,
              default_with_serializer: { "foo" => "bar" }
            )

            expect(person.default_with_serializer).to eq({ "foo" => "bar" })
            person.save!
            person.reload
            expect(person.default_with_serializer).to eq({ "foo" => "bar" })
          end
        end
      end
    end
  end

  context "with the :json serializer"  do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_details")
    end

    it "does not default to a hash" do
      person = Person.new
      expect(person.details).to eq(nil)
      person.save!
      person.reload
      expect(person.details).to eq(nil)
    end

    it "tracks dirty attributes" do
      person = Person.create!(details: { "foo" => "bar" })

      expect(person.details_changed?).to be(false)
      expect(person.details_change).to be(nil)
      expect(person.details_was).to eq({ "foo" => "bar" })

      person.details = { "zip" => "zap" }

      expect(person.details_changed?).to be(true)
      expect(person.details_change).to eq([{ "foo" => "bar" }, { "zip" => "zap" }])
      expect(person.details_was).to eq({ "foo" => "bar" })
    end

    it "encodes and decodes attributes" do
      person = Person.create!(details: { "foo" => "bar", "zip" => 1 })
      person.reload

      raw = Vault::Rails.decrypt("transit", "dummy_people_details", person.details_encrypted)
      expect(raw).to eq("{\"foo\":\"bar\",\"zip\":1}")

      expect(person.details).to eq("foo" => "bar", "zip" => 1)
    end
  end

  context "with a custom serializer" do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_business_card")
    end

    it "encodes and decodes attributes" do
      person = Person.create!(business_card: "data")
      person.reload

      raw = Vault::Rails.decrypt("transit", "dummy_people_business_card", person.business_card_encrypted)
      expect(raw).to eq("01100100011000010111010001100001")

      expect(person.business_card).to eq("data")
    end
  end

  context "with custom encode/decode proc" do
    before(:all) do
      Vault::Rails.logical.write("transit/keys/dummy_people_favorite_color")
    end

    it "encodes and decodes attributes" do
      person = Person.create!(favorite_color: "blue")
      person.reload

      raw = Vault::Rails.decrypt("transit", "dummy_people_favorite_color", person.favorite_color_encrypted)
      expect(raw).to eq("xxxbluexxx")

      expect(person.favorite_color).to eq("blue")
    end
  end

  xcontext "with context" do
    it "encodes and decodes with a string context" do
      person = Person.create!(context_string: "foobar")
      person.reload

      raw = Vault::Rails.decrypt(
        "transit", "dummy_people_context_string",
        person.context_string_encrypted, context: "production")

      expect(raw).to eq("foobar")

      expect(person.context_string).to eq("foobar")

      # Decrypting without the correct context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_string",
          person.context_string_encrypted, context: "wrongcontext")
      }.to raise_error(Vault::HTTPClientError, /invalid ciphertext/)

      # Decrypting without a context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_string",
          person.context_string_encrypted)
      }.to raise_error(Vault::HTTPClientError, /context/)
    end

    it "encodes and decodes with a symbol context" do
      person = Person.create!(context_symbol: "foobar")
      person.reload

      raw = Vault::Rails.decrypt(
        "transit", "dummy_people_context_symbol",
        person.context_symbol_encrypted, context: person.encryption_context)

      expect(raw).to eq("foobar")

      expect(person.context_symbol).to eq("foobar")

      # Decrypting without the correct context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_symbol",
          person.context_symbol_encrypted, context: "wrongcontext")
      }.to raise_error(Vault::HTTPClientError, /invalid ciphertext/)

      # Decrypting without a context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_symbol",
          person.context_symbol_encrypted)
      }.to raise_error(Vault::HTTPClientError, /context/)
    end

    it "encodes and decodes with a proc context" do
      person = Person.create!(context_proc: "foobar")
      person.reload

      raw = Vault::Rails.decrypt(
        "transit", "dummy_people_context_proc",
        person.context_proc_encrypted, context: person.encryption_context)

      expect(raw).to eq("foobar")

      expect(person.context_proc).to eq("foobar")

      # Decrypting without the correct context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_proc",
          person.context_proc_encrypted, context: "wrongcontext")
      }.to raise_error(Vault::HTTPClientError, /invalid ciphertext/)

      # Decrypting without a context fails
      expect {
        Vault::Rails.decrypt(
          "transit", "dummy_people_context_proc",
          person.context_proc_encrypted)
      }.to raise_error(Vault::HTTPClientError, /context/)
    end
  end

  xcontext 'with transform_secret', ent_vault: ">= 1.4" do
    before(:all) do
      Vault::Rails.sys.mount("transform", :transform)
      Vault::Rails.client.transform.create_transformation(
        "social_sec",
        template: "builtin/socialsecuritynumber",
        tweak_source: "internal",
        type: "fpe",
        allowed_roles: [Vault::Rails.application]
      )
      Vault::Rails.client.transform.create_role(Vault::Rails.application, transformations: ["social_sec"])
      Vault::Rails.client.transform.create_role("foobar_role", transformations: ["social_sec"])
    end

    it "encrypts the attribute using the given transformation" do
      person = Person.create!(transform_ssn: "123-45-6789")
      expect(person[:transform_ssn_encrypted]).not_to eq("123-45-6789")
      expect(person[:transform_ssn_encrypted]).to match(/\d{3}-\d{2}-\d{4}/)
      expect(person.transform_ssn).to eq("123-45-6789")
    end

    it "raises an error if the format is incorrect" do
      expect{ Person.create!(transform_ssn: "1234-5678-90") }.to(
        raise_error(Vault::HTTPClientError, /unable to find matching expression/)
      )
    end

    it "raises an error if the transformation does not exist" do
      expect{ Person.create!(bad_transform: "nope") }.to(
        raise_error(Vault::HTTPClientError, /unable to find transformation/)
      )
    end

    it "raises an error if the provided role doesn't have the ability to use the transformation" do
      expect{ Person.create!(bad_role_transform: "123-45-6789") }.to(
        raise_error(Vault::HTTPClientError, /is not an allowed role for the transformation/)
      )
    end
  end

  context 'with errors' do
    it 'raises the appropriate exception' do
      expect {
        Vault::Rails.encrypt('/bogus/path', 'bogus', 'bogus')
      }.to raise_error(Vault::HTTPClientError)
    end
  end

  context "without a server" do
    it "encrypts attributes with a dev prefix" do
      allow(Vault::Rails).to receive(:enabled?).and_return(false)
      person = Person.create!(credit_card: "1234567890111213")
      expect(person.cc_encrypted).to start_with(Vault::Rails::DEV_PREFIX)
    end

    it "decrypts attributes" do
      allow(Vault::Rails).to receive(:enabled?).and_return(false)
      person = Person.create!(credit_card: "1234567890111213")
      person.reload
      expect(person.credit_card).to eq("1234567890111213")
    end
  end
end
