#
# Author:: Tyler Cloke (tyler@chef.io)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# 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.
#

require "spec_helper"

require "chef/key"

describe Chef::Key do
  # whether user or client irrelevant to these tests
  let(:key) { Chef::Key.new("original_actor", "user") }
  let(:public_key_string) do
    <<~EOS
      -----BEGIN PUBLIC KEY-----
      MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPo+oNPB7uuNkws0fC02
      KxSwdyqPLu0fhI1pOweNKAZeEIiEz2PkybathHWy8snSXGNxsITkf3eyvIIKa8OZ
      WrlqpI3yv/5DOP8HTMCxnFuMJQtDwMcevlqebX4bCxcByuBpNYDcAHjjfLGSfMjn
      E5lZpgYWwnpic4kSjYcL9ORK9nYvlWV9P/kCYmRhIjB4AhtpWRiOfY/TKi3P2LxT
      IjSmiN/ihHtlhV/VSnBJ5PzT/lRknlrJ4kACoz7Pq9jv+aAx5ft/xE9yDa2DYs0q
      Tfuc9dUYsFjptWYrV6pfEQ+bgo1OGBXORBFcFL+2D7u9JYquKrMgosznHoEkQNLo
      0wIDAQAB
      -----END PUBLIC KEY-----
    EOS
  end

  shared_examples_for "fields with username type validation" do
    context "when invalid input is passed" do
      # It is not feasible to check all invalid characters.  Here are a few
      # that we probably care about.
      it "should raise an ArgumentError" do
        # capital letters
        expect { key.send(field, "Bar") }.to raise_error(ArgumentError)
        # slashes
        expect { key.send(field, "foo/bar") }.to raise_error(ArgumentError)
        # ?
        expect { key.send(field, "foo?") }.to raise_error(ArgumentError)
        # &
        expect { key.send(field, "foo&") }.to raise_error(ArgumentError)
        # spaces
        expect { key.send(field, "foo ") }.to raise_error(ArgumentError)
      end
    end
  end

  shared_examples_for "string fields that are settable" do
    context "when it is set with valid input" do
      it "should set the field" do
        key.send(field, valid_input)
        expect(key.send(field)).to eq(valid_input)
      end
    end

    context "when you feed it anything but a string" do
      it "should raise an ArgumentError" do
        expect { key.send(field, {}) }.to raise_error(ArgumentError)
      end
    end
  end

  describe "when a new Chef::Key object is initialized with invalid input" do
    it "should raise an InvalidKeyArgument" do
      expect { Chef::Key.new("original_actor", "not_a_user_or_client") }.to raise_error(Chef::Exceptions::InvalidKeyArgument)
    end
  end

  describe "when a new Chef::Key object is initialized with valid input" do
    it "should be a Chef::Key" do
      expect(key).to be_a_kind_of(Chef::Key)
    end

    it "should properly set the actor" do
      expect(key.actor).to eq("original_actor")
    end
  end

  describe "when actor field is set" do
    it_should_behave_like "string fields that are settable" do
      let(:field) { :actor }
      let(:valid_input) { "new_field_value" }
    end

    it_should_behave_like "fields with username type validation" do
      let(:field) { :actor }
    end
  end

  describe "when the name field is set" do
    it_should_behave_like "string fields that are settable" do
      let(:field) { :name }
      let(:valid_input) { "new_field_value" }
    end
  end

  describe "when the private_key field is set" do
    it_should_behave_like "string fields that are settable" do
      let(:field) { :private_key }
      let(:valid_input) { "new_field_value" }
    end
  end

  describe "when the public_key field is set" do
    it_should_behave_like "string fields that are settable" do
      let(:field) { :public_key }
      let(:valid_input) { "new_field_value" }
    end

    context "when create_key is true" do
      before do
        key.create_key true
      end

      it "should raise an InvalidKeyAttribute" do
        expect { key.public_key public_key_string }.to raise_error(Chef::Exceptions::InvalidKeyAttribute)
      end
    end
  end

  describe "when the create_key field is set" do
    context "when it is set to true" do
      it "should set the field" do
        key.create_key(true)
        expect(key.create_key).to eq(true)
      end
    end

    context "when it is set to false" do
      it "should set the field" do
        key.create_key(false)
        expect(key.create_key).to eq(false)
      end
    end

    context "when anything but a TrueClass or FalseClass is passed" do
      it "should raise an ArgumentError" do
        expect { key.create_key "not_a_boolean" }.to raise_error(ArgumentError)
      end
    end

    context "when public_key is defined" do
      before do
        key.public_key public_key_string
      end

      it "should raise an InvalidKeyAttribute" do
        expect { key.create_key true }.to raise_error(Chef::Exceptions::InvalidKeyAttribute)
      end
    end
  end

  describe "when the expiration_date field is set" do
    context "when a valid date is passed" do
      it_should_behave_like "string fields that are settable" do
        let(:field) { :public_key }
        let(:valid_input) { "2020-12-24T21:00:00Z" }
      end
    end

    context "when infinity is passed" do
      it_should_behave_like "string fields that are settable" do
        let(:field) { :public_key }
        let(:valid_input) { "infinity" }
      end
    end

    context "when an invalid date is passed" do
      it "should raise an ArgumentError" do
        expect { key.expiration_date "invalid_date" }.to raise_error(ArgumentError)
        # wrong years
        expect { key.expiration_date "20-12-24T21:00:00Z" }.to raise_error(ArgumentError)
      end

      context "when it is a valid UTC date missing a Z" do
        it "should raise an ArgumentError" do
          expect { key.expiration_date "2020-12-24T21:00:00" }.to raise_error(ArgumentError)
        end
      end
    end
  end # when the expiration_date field is set

  describe "when serializing to JSON" do
    shared_examples_for "common json operations" do
      it "should serializes as a JSON object" do
        expect(json).to match(/^\{.+\}$/)
      end

      it "should include the actor value under the key relative to the actor_field_name passed" do
        expect(json).to include(%Q{"#{new_key.actor_field_name}":"original_actor"})
      end

      it "should include the name field when present" do
        new_key.name("monkeypants")
        expect(new_key.to_json).to include(%q{"name":"monkeypants"})
      end

      it "should not include the name if not present" do
        expect(json).to_not include("name")
      end

      it "should include the public_key field when present" do
        new_key.public_key "this_public_key"
        expect(new_key.to_json).to include(%q{"public_key":"this_public_key"})
      end

      it "should not include the public_key if not present" do
        expect(json).to_not include("public_key")
      end

      it "should include the private_key field when present" do
        new_key.private_key "this_public_key"
        expect(new_key.to_json).to include(%q{"private_key":"this_public_key"})
      end

      it "should not include the private_key if not present" do
        expect(json).to_not include("private_key")
      end

      it "should include the expiration_date field when present" do
        new_key.expiration_date "2020-12-24T21:00:00Z"
        expect(new_key.to_json).to include(%q{"expiration_date":"2020-12-24T21:00:00Z"})
      end

      it "should not include the expiration_date if not present" do
        expect(json).to_not include("expiration_date")
      end

      it "should include the create_key field when present" do
        new_key.create_key true
        expect(new_key.to_json).to include(%q{"create_key":true})
      end

      it "should not include the create_key if not present" do
        expect(json).to_not include("create_key")
      end
    end

    context "when key is for a user" do
      it_should_behave_like "common json operations" do
        let(:new_key) { Chef::Key.new("original_actor", "user") }
        let(:json) do
          new_key.to_json
        end
      end
    end

    context "when key is for a client" do
      it_should_behave_like "common json operations" do
        let(:new_key) { Chef::Key.new("original_actor", "client") }
        let(:json) do
          new_key.to_json
        end
      end
    end

  end # when serializing to JSON

  describe "when deserializing from JSON" do
    shared_examples_for "a deserializable object" do
      it "deserializes to a Chef::Key object" do
        expect(key).to be_a_kind_of(Chef::Key)
      end

      it "preserves the actor" do
        expect(key.actor).to eq("turtle")
      end

      it "preserves the name" do
        expect(key.name).to eq("key_name")
      end

      it "includes the public key if present" do
        expect(key.public_key).to eq(public_key_string)
      end

      it "includes the expiration_date if present" do
        expect(key.expiration_date).to eq("infinity")
      end

      it "includes the private_key if present" do
        expect(key.private_key).to eq("some_private_key")
      end

      it "includes the create_key if present" do
        expect(key_with_create_key_field.create_key).to eq(true)
      end
    end

    context "when deserializing a key for a user" do
      it_should_behave_like "a deserializable object" do
        let(:key) do
          o = { "user" => "turtle",
                "name" => "key_name",
                "public_key" => public_key_string,
                "private_key" => "some_private_key",
                "expiration_date" => "infinity" }
          Chef::Key.from_json(o.to_json)
        end
        let(:key_with_create_key_field) do
          o = { "user" => "turtle",
                "create_key" => true }
          Chef::Key.from_json(o.to_json)
        end
      end
    end

    context "when deserializing a key for a client" do
      it_should_behave_like "a deserializable object" do
        let(:key) do
          o = { "client" => "turtle",
                "name" => "key_name",
                "public_key" => public_key_string,
                "private_key" => "some_private_key",
                "expiration_date" => "infinity" }
          Chef::Key.from_json(o.to_json)
        end
        let(:key_with_create_key_field) do
          o = { "client" => "turtle",
                "create_key" => true }
          Chef::Key.from_json(o.to_json)
        end
      end
    end
  end # when deserializing from JSON

  describe "API Interactions" do
    let(:rest) do
      Chef::Config[:chef_server_root] = "http://www.example.com"
      Chef::Config[:chef_server_url] = "http://www.example.com/organizations/test_org"
      r = double("rest")
      allow(Chef::ServerAPI).to receive(:new).and_return(r)
      r
    end

    let(:user_key) do
      o = Chef::Key.new("foobar", "user")
      o
    end

    let(:client_key) do
      o = Chef::Key.new("foobar", "client")
      o
    end

    describe "list" do
      context "when listing keys for a user" do
        let(:response) { [{ "uri" => "http://www.example.com/users/keys/foobar", "name" => "foobar", "expired" => false }] }
        let(:inflated_response) { { "foobar" => user_key } }

        it "lists all keys" do
          expect(rest).to receive(:get).with("users/#{user_key.actor}/keys").and_return(response)
          expect(Chef::Key.list_by_user("foobar")).to eq(response)
        end

        it "inflate all keys" do
          allow(Chef::Key).to receive(:load_by_user).with(user_key.actor, "foobar").and_return(user_key)
          expect(rest).to receive(:get).with("users/#{user_key.actor}/keys").and_return(response)
          expect(Chef::Key.list_by_user("foobar", true)).to eq(inflated_response)
        end

      end

      context "when listing keys for a client" do
        let(:response) { [{ "uri" => "http://www.example.com/users/keys/foobar", "name" => "foobar", "expired" => false }] }
        let(:inflated_response) { { "foobar" => client_key } }

        it "lists all keys" do
          expect(rest).to receive(:get).with("clients/#{client_key.actor}/keys").and_return(response)
          expect(Chef::Key.list_by_client("foobar")).to eq(response)
        end

        it "inflate all keys" do
          allow(Chef::Key).to receive(:load_by_client).with(client_key.actor, "foobar").and_return(client_key)
          expect(rest).to receive(:get).with("clients/#{user_key.actor}/keys").and_return(response)
          expect(Chef::Key.list_by_client("foobar", true)).to eq(inflated_response)
        end

      end
    end

    describe "create" do
      shared_examples_for "create key" do
        context "when a field is missing" do
          it "should raise a MissingKeyAttribute" do
            expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute)
          end
        end

        context "when the name field is missing" do
          before do
            key.public_key public_key_string
            key.expiration_date "2020-12-24T21:00:00Z"
          end

          it "creates a new key via the API with the fingerprint as the name" do
            expect(rest).to receive(:post).with(url,
              { "name" => "12:3e:33:73:0b:f4:ec:72:dc:f0:4c:51:62:27:08:76:96:24:f4:4a",
                "public_key" => key.public_key,
                "expiration_date" => key.expiration_date }).and_return({})
            key.create
          end
        end

        context "when every field is populated" do
          before do
            key.name "key_name"
            key.public_key public_key_string
            key.expiration_date "2020-12-24T21:00:00Z"
            key.create_key false
          end

          context "when create_key is false" do
            it "creates a new key via the API" do
              expect(rest).to receive(:post).with(url,
                { "name" => key.name,
                  "public_key" => key.public_key,
                  "expiration_date" => key.expiration_date }).and_return({})
              key.create
            end
          end

          context "when create_key is true and public_key is nil" do

            before do
              key.delete_public_key
              key.create_key true
              $expected_output = {
                actor_type => "foobar",
                "name" => key.name,
                "create_key" => true,
                "expiration_date" => key.expiration_date,
              }
              $expected_input = {
                "name" => key.name,
                "create_key" => true,
                "expiration_date" => key.expiration_date,
              }
            end

            it "should create a new key via the API" do
              expect(rest).to receive(:post).with(url, $expected_input).and_return({})
              key.create
            end

            context "when the server returns the private_key via key.create" do
              before do
                allow(rest).to receive(:post).with(url, $expected_input).and_return({ "private_key" => "this_private_key" })
              end

              it "key.create returns the original key plus the private_key" do
                expect(key.create.to_hash).to eq($expected_output.merge({ "private_key" => "this_private_key" }))
              end
            end
          end

          context "when create_key is false and public_key is nil" do
            before do
              key.delete_public_key
              key.create_key false
            end
            it "should raise an InvalidKeyArgument" do
              expect { key.create }.to raise_error(Chef::Exceptions::MissingKeyAttribute)
            end
          end
        end
      end

      context "when creating a user key" do
        it_should_behave_like "create key" do
          let(:url) { "users/#{key.actor}/keys" }
          let(:key) { user_key }
          let(:actor_type) { "user" }
        end
      end

      context "when creating a client key" do
        it_should_behave_like "create key" do
          let(:url) { "clients/#{client_key.actor}/keys" }
          let(:key) { client_key }
          let(:actor_type) { "client" }
        end
      end
    end # create

    describe "update" do
      shared_examples_for "update key" do
        context "when name is missing and no argument was passed to update" do
          it "should raise an MissingKeyAttribute" do
            expect { key.update }.to raise_error(Chef::Exceptions::MissingKeyAttribute)
          end
        end

        context "when some fields are populated" do
          before do
            key.name "key_name"
            key.expiration_date "2020-12-24T21:00:00Z"
          end

          it "should update the key via the API" do
            expect(rest).to receive(:put).with(url, key.to_hash).and_return({})
            key.update
          end
        end

        context "when @name is not nil and a arg is passed to update" do
          before do
            key.name "new_name"
          end

          it "passes @name in the body and the arg in the PUT URL" do
            expect(rest).to receive(:put).with(update_name_url, key.to_hash).and_return({})
            key.update("old_name")
          end
        end

        context "when the server returns a public_key and create_key is true" do
          before do
            key.name "key_name"
            key.create_key true
            allow(rest).to receive(:put).with(url, key.to_hash).and_return({
                                                                                  "key" => "key_name",
                                                                                  "public_key" => public_key_string,
                                                                                })

          end

          it "returns a key with public_key populated" do
            new_key = key.update
            expect(new_key.public_key).to eq(public_key_string)
          end

          it "returns a key without create_key set" do
            new_key = key.update
            expect(new_key.create_key).to be_nil
          end
        end
      end

      context "when updating a user key" do
        it_should_behave_like "update key" do
          let(:url) { "users/#{key.actor}/keys/#{key.name}" }
          let(:update_name_url) { "users/#{key.actor}/keys/old_name" }
          let(:key) { user_key }
        end
      end

      context "when updating a client key" do
        it_should_behave_like "update key" do
          let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" }
          let(:update_name_url) { "clients/#{client_key.actor}/keys/old_name" }
          let(:key) { client_key }
        end
      end

    end # update

    describe "load" do
      shared_examples_for "load" do
        it "should load a named key from the API" do
          expect(rest).to receive(:get).with(url).and_return({ "user" => "foobar", "name" => "test_key_name", "public_key" => public_key_string, "expiration_date" => "infinity" })
          key = Chef::Key.send(load_method, "foobar", "test_key_name")
          expect(key.actor).to eq("foobar")
          expect(key.name).to eq("test_key_name")
          expect(key.public_key).to eq(public_key_string)
          expect(key.expiration_date).to eq("infinity")
        end
      end

      describe "load_by_user" do
        it_should_behave_like "load" do
          let(:load_method) { :load_by_user }
          let(:url) { "users/foobar/keys/test_key_name" }
        end
      end

      describe "load_by_client" do
        it_should_behave_like "load" do
          let(:load_method) { :load_by_client }
          let(:url) { "clients/foobar/keys/test_key_name" }
        end
      end

    end # load

    describe "destroy" do
      shared_examples_for "destroy key" do
        context "when name is missing" do
          it "should raise an MissingKeyAttribute" do
            expect { Chef::Key.new("username", "user").destroy }.to raise_error(Chef::Exceptions::MissingKeyAttribute)
          end
        end

        before do
          key.name "key_name"
        end
        context "when name is not missing" do
          it "should delete the key via the API" do
            expect(rest).to receive(:delete).with(url).and_return({})
            key.destroy
          end
        end
      end

      context "when destroying a user key" do
        it_should_behave_like "destroy key" do
          let(:url) { "users/#{key.actor}/keys/#{key.name}" }
          let(:key) { user_key }
        end
      end

      context "when destroying a client key" do
        it_should_behave_like "destroy key" do
          let(:url) { "clients/#{client_key.actor}/keys/#{key.name}" }
          let(:key) { client_key }
        end
      end
    end
  end # API Interactions
end
