# 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 "pedant/rspec/data_bag_util"
describe "Data Bag API endpoint", :data_bags do
  include Pedant::RSpec::DataBagUtil

  # Just until we rename the requestors
  let(:admin_requestor) { admin_user }

  let(:requestor) { admin_requestor }
  let(:data_bags_url) { api_url("/data") }
  let(:named_data_bag_url) { api_url("/data/#{data_bag_name}") }
  let(:data_bag_item_url) { api_url("/data/#{data_bag_name}/#{data_bag_item_id}") }

  context "with no data bags" do
    context "a request to /data" do
      context "GET" do
        it "returns an empty list of data bags" do
          get(data_bags_url, requestor).should look_like fetch_data_bag_list_empty_response
        end
      end # GET
      context "POST" do
        let(:request_method) { :POST }

        let(:data_bag_name) { unique_name("testbag") }
        let(:data_bag_url) { api_url("/data/#{data_bag_name}") }
        let(:request_url) { data_bag_url }
        let(:data_bag) { new_data_bag(data_bag_name) }

        after :each do
          delete(data_bag_url, admin_requestor)
        end

        context "with a canonical payload", :smoke do
          it_behaves_like "a successful data bag POST"
        end

        context "valid requests of various types to create a data bag" do
          context "with a valid name" do
            names = ["pedant", "pedant-bag", "pedant_bag", "pedant_bag-foo", "1234567890", "pedant99", "pedant:with:colons", "pedant.with.dots"]

            names.each do |n|
              context "like '#{n}'" do
                let(:data_bag_name) { n }
                it_behaves_like "a successful data bag POST"
              end
            end
          end

          context "missing certain keys" do
            %w{json_class chef_type}.each do |key|
              context "without a '#{key}' key" do
                let(:data_bag) {
                  new_data_bag(data_bag_name).tap { |b| b.delete key }
                }
                it_behaves_like "a successful data bag POST"
              end
            end
          end

          context "with just a name" do
            let(:data_bag) { { "name" => data_bag_name } }
            it_behaves_like "a successful data bag POST"
          end

          context "with an incorrect 'chef_type' key" do
            let(:data_bag) {
              new_data_bag(data_bag_name).tap { |b| b["chef_type"] = "node" }
            }
            it "does not have the correct chef_type key" do
              # Just a touch of paranoia
              data_bag["chef_type"].should_not eq "data_bag"
            end
            it_behaves_like "a successful data bag POST"
          end

          context "with an incorrect 'json_class' key" do
            let(:data_bag) {
              new_data_bag(data_bag_name).tap { |b| b["json_class"] = "Chef::Node" }
            }
            it "does not have the correct json_class key" do
              # Just a touch of paranoia
              data_bag["json_class"].should_not eq "Chef::DataBag"
            end
            it_behaves_like "a successful data bag POST"
          end
        end # valid requests

        context "invalid requests of various types to create a data bag", :validation do

          context "with an invalid name" do
            bad_names = ["pedant_badName!!$$$$_oh_very+bad", "pedant-does-not-like-punctuation!!!!"]
            bad_names.each do |bad_name|
              context "like '#{bad_name}'" do
                let(:data_bag_name) { bad_name }
                let(:expected_failure_response) { create_data_bag_bad_name_failure_response }
                it_behaves_like "an unsuccessful data bag POST"
              end
            end
          end

          context "without a name" do
            let(:data_bag) { { "id" => data_bag_name } }
            let(:expected_failure_response) { create_data_bag_no_name_failure_response }
            it_behaves_like "an unsuccessful data bag POST"
          end
        end # invalid requests

        respects_maximum_payload_size
      end # POST

      context "PUT" do
        let(:put_body) { { "fake" => "value" } }
        it "is not allowed" do
          put(data_bags_url, requestor, payload: put_body).should look_like data_endpoint_method_not_allowed_response
        end
      end # PUT

      context "DELETE" do
        it "is not allowed" do
          delete(data_bags_url, requestor).should look_like data_endpoint_method_not_allowed_response
        end
      end # DELETE
    end # request to /data

    context "a request to /data/<bag>" do
      let(:data_bag_name) { unique_name("no_bag") }
      context "GET" do
        it "fails because there is no bag" do
          get(named_data_bag_url, requestor).should look_like data_bag_not_found_response
        end
      end
      context "POST" do
        let(:post_body) { { "id" => "blah" } }
        it "fails because there is no bag" do
          post(named_data_bag_url, requestor, payload: post_body).should look_like create_data_bag_item_no_data_bag_response
        end
      end
      context "PUT" do
        let(:put_body) { { "fake" => "value" } }
        it "is not allowed" do
          put(named_data_bag_url, requestor,
            payload: put_body).should look_like data_bag_endpoint_method_not_allowed_response
        end
      end
      context "DELETE" do
        it "fails because there is no bag" do
          delete(named_data_bag_url, requestor).should look_like data_bag_not_found_response
        end
      end
    end #  'a request to /data/<bag>'
    context "a request to /data/<bag>/<item>" do
      let(:data_bag_name) { unique_name("no_bag") }
      let(:data_bag_item_id) { unique_name("no_item") }
      context "GET" do
        it "fails because there is no bag" do
          get(data_bag_item_url, requestor).should look_like data_bag_item_not_found_no_bag_response
        end
      end
      context "POST" do
        let(:post_body) { { "fake" => "value" } }
        it "is not allowed" do
          post(data_bag_item_url, requestor, payload: post_body).should look_like data_bag_item_endpoint_method_not_allowed_response
        end
      end
      context "PUT" do
        let(:put_body) { { "id" => data_bag_item_id } }
        it "fails because there is no bag" do
          put(data_bag_item_url, requestor, payload: put_body).should look_like data_bag_item_not_found_no_bag_response
        end
      end
      context "DELETE" do
        it "fails because there is no bag" do
          delete(data_bag_item_url, requestor).should look_like data_bag_item_not_found_from_delete_no_bag_response
        end
      end
    end #  'a request to /data/<bag>'
  end # with no data bags
  context "with data bags" do
    let(:data_bag_1_name) { unique_name("bag1") }
    let(:data_bag_2_name) { unique_name("bag2") }
    let(:data_bag_1) { new_data_bag(data_bag_1_name) }
    let(:data_bag_2) { new_data_bag(data_bag_2_name) }
    before :each do
      create_data_bag(admin_requestor, data_bag_1)
      create_data_bag(admin_requestor, data_bag_2)
    end
    after :each do
      delete_data_bag(admin_requestor, data_bag_1_name)
      delete_data_bag(admin_requestor, data_bag_2_name)
    end
    context "that have no items" do
      context "a request to /data" do
        context "GET" do
          it "returns a non-empty list of data bags" do
            get(data_bags_url, requestor).should look_like fetch_data_bag_list_full_response
          end
        end # GET
        context "POST" do
          let(:data_bag_name) { data_bag_1_name }
          let(:data_bag) { data_bag_1 }
          it "raises a conflict when creating an existing data bag" do
            post(data_bags_url, requestor, payload: data_bag_1).should look_like create_data_bag_conflict_response
          end
        end # POST
        # PUT isn't allowed, but that's already been tested
        # DELETE isn't allowed, but that's already been tested
      end # request to /data
      context "a request to /data/<bag>" do
        let(:data_bag_name) { data_bag_1_name }

        context "GET" do
          it "returns an empty bag" do
            get(named_data_bag_url, requestor).should look_like fetch_empty_data_bag_success_response
          end
        end
        context "POST" do
          let(:request_method) { :POST }
          let(:data_bag_item_id) { unique_name("item") }
          let(:request_url) { api_url("/data/#{data_bag_name}") }

          after :each do
            delete_data_bag_item(admin_requestor, data_bag_name, data_bag_item_id)
          end
          context "various good inputs to create a data bag item" do
            context "with JUST an ID", :smoke do
              let(:data_bag_item) { { "id" => data_bag_item_id } }
              it_behaves_like "a successful data bag item POST"
            end

            context "with a good ID" do
              ids = %w{pedantitem pedant_item pedant-item pedant-123-item}

              ids += ["pedant:item", "pedant.item"]

              ids.each do |i|
                context "like '#{i}'" do
                  let(:data_bag_item_id) { i }
                  let(:data_bag_item) { { "id" => data_bag_item_id, "answer" => 42 } }
                  let(:expected_failure_response) { create_data_bag_item_invalid_id_response }
                  it_behaves_like "a successful data bag item POST"
                end
              end
            end
          end

          context "various bad inputs to create a data bag item", :validation do
            context "without an ID" do
              let(:data_bag_item) { { "answer" => 42 } }
              let(:expected_failure_response) { create_data_bag_item_no_id_response }
              it "really does not have an id" do
                data_bag_item.should_not have_key "id"
              end
              it_behaves_like "an unsuccessful data bag item POST"
            end

            context "with a malformed ID" do
              ids = ["pedant_badId!!", "^$@^*  pedant"]
              ids.each do |i|
                context "like '#{i}'" do
                  let(:data_bag_item_id) { i }
                  let(:data_bag_item) { { "id" => data_bag_item_id, "answer" => 42 } }
                  let(:expected_failure_response) { create_data_bag_item_invalid_id_response }
                  it_behaves_like "an unsuccessful data bag item POST"
                end
              end
            end
          end

          respects_maximum_payload_size
        end

        # PUT isn't allowed, but that's already been tested
        context "DELETE" do
          it "deletes the data bag" do
            # bag is deleted
            delete(named_data_bag_url, requestor).should look_like delete_data_bag_success_response
            # it's not there anymore
            get(named_data_bag_url, requestor).should look_like data_bag_not_found_response
            # the other bag is fine
            get(api_url("/data/#{data_bag_2_name}"), requestor).should look_like ok_response
          end
        end
      end
      context "a request to /data/<bag>/<item>" do
        let(:data_bag_name) { data_bag_1_name }
        let(:data_bag_item_id) { unique_name("no_item") }
        context "GET" do
          it "fails because there is no item" do
            get(data_bag_item_url, requestor).should look_like data_bag_item_not_found_response
          end
        end
        # POST is not allowed, but that's already been tested
        context "PUT" do
          let(:put_body) { { "id" => data_bag_item_id } }
          it "fails because there is no item" do
            put(data_bag_item_url, requestor, payload: put_body).should look_like data_bag_item_not_found_from_put_response
          end
        end
        context "DELETE" do
          it "fails because there is no item" do
            delete(data_bag_item_url, requestor).should look_like data_bag_item_not_found_from_delete_response
          end
        end
      end # request to /data/<bag>/<item>
    end # that have no items
    context "that have items" do
      let(:data_bag_item_1_id) { unique_name("item1") }
      let(:data_bag_item_2_id) { unique_name("item2") }
      let(:data_bag_item_3_id) { unique_name("item3") }

      let(:data_bag_item_1) { new_data_bag_item(data_bag_item_1_id) }
      let(:data_bag_item_2) { new_data_bag_item(data_bag_item_2_id) }
      let(:data_bag_item_3) { new_data_bag_item(data_bag_item_3_id) }

      let(:data_bag_name) { data_bag_1_name }
      let(:data_bag_item_id) { data_bag_item_1_id }
      let(:data_bag_item) { data_bag_item_1 }

      before :each do
        create_data_bag_item(admin_requestor, data_bag_name, data_bag_item_1)
        create_data_bag_item(admin_requestor, data_bag_name, data_bag_item_2)
        create_data_bag_item(admin_requestor, data_bag_name, data_bag_item_3)
      end

      after :each do
        delete_data_bag_item(admin_requestor, data_bag_name, data_bag_item_1_id)
        delete_data_bag_item(admin_requestor, data_bag_name, data_bag_item_2_id)
        delete_data_bag_item(admin_requestor, data_bag_name, data_bag_item_3_id)
      end

      context "a request to /data" do
        context "GET" do
          it "returns the same non-empty list of data bags" do
            get(data_bags_url, requestor).should look_like fetch_data_bag_list_full_response
          end
        end
        # No other cases to try for POST
        # PUT is not allowed, but that's already been tested
        # DELETE is not allowed, but that's already been tested
      end # /data
      context "a request to /data/<bag>" do
        context "GET" do
          it "shows a full data bag", :smoke do
            get(named_data_bag_url, requestor).should look_like fetch_full_data_bag_success_response
          end
        end
        context "POST" do
          let(:data_bag_item) { data_bag_item_1 }
          it "raises a conflict when creating an existing data bag item" do
            post(named_data_bag_url, requestor,
              payload: data_bag_item).should look_like create_data_bag_item_conflict_response
          end
        end
        # PUT is not allowed, but that's already been tested
        context "DELETE", :smoke do
          it "deletes a bag AND ALL THE ITEMS" do
            # bag is deleted
            delete(named_data_bag_url, requestor).should look_like delete_data_bag_success_response
            # it's not there anymore
            get(named_data_bag_url, requestor).should look_like data_bag_not_found_response
            # the other bag is fine
            get(api_url("/data/#{data_bag_2_name}"), requestor).should look_like ok_response
            # All the items are gone now
            [data_bag_item_1_id, data_bag_item_2_id, data_bag_item_3_id].each do |i|
              get(api_url("/data/#{data_bag_name}/#{i}"), requestor).should look_like resource_not_found_response
            end
          end
        end
      end # /data/bag
      context "a request to /data/<bag>/<item>" do
        context "GET" do
          let(:data_bag_item_id) { data_bag_item_1_id }
          let(:data_bag_item) { data_bag_item_1 }
          it "shows the complete item", :smoke do
            get(data_bag_item_url, requestor).should look_like fetch_data_bag_item_success_response
          end
        end
        # POST is not allowed, but that's already been tested
        context "PUT" do
          let(:request_method) { :PUT }
          let(:request_url) { data_bag_item_url }
          context "with various correct inputs to update a data bag item" do

            context "with normal input", :smoke do
              let(:updated_data_bag_item) { { "id" => data_bag_item_id, "new_stuff" => "foo" } }
              it_behaves_like "a successful data bag item PUT"
            end

            context "with no ID in the update body" do
              let(:updated_data_bag_item) { { "id" => data_bag_item_id, "new_stuff" => "foo" } }

              it "successfully updates (using ID from the URL)" do
                updated_data_bag_item.delete "id"
                updated_data_bag_item.should_not have_key "id"

                put(data_bag_item_url, requestor,
                  payload: updated_data_bag_item).should look_like update_data_bag_item_missing_id_response
              end
            end
          end
          context "with various incorrect inputs to update a data bag item" do

            context "to an item with a different id" do
              let(:different_data_bag_id) { "different" }
              let(:updated_data_bag_item) { { "id" => different_data_bag_id, "new_stuff" => "foo" } }

              let(:expected_failure_response) { update_data_bag_item_mismatched_id_response }
              it "should have a different id" do
                updated_data_bag_item["id"].should_not eq data_bag_item_id
              end
              it_behaves_like "an unsuccessful data bag item PUT"
            end
          end

          respects_maximum_payload_size

        end
        context "DELETE" do
          let(:data_bag_item_id) { data_bag_item_1_id }
          it "deletes the item", :smoke do
            # delete the item
            delete(data_bag_item_url, requestor).should look_like delete_data_bag_item_success_response
            # it can't be retrieved
            get(data_bag_item_url, requestor).should look_like resource_not_found_response
            # other items are left alone
            [data_bag_item_2_id, data_bag_item_3_id].each do |i|
              get(api_url("/data/#{data_bag_name}/#{i}"), requestor).should look_like ok_response
            end
            # and the data bag itself looks sane
            get(named_data_bag_url, requestor).should look_like({
                                                                  status: 200,
                                                                  body_exact: {
                                                                    data_bag_item_2_id => api_url("/data/#{data_bag_name}/#{data_bag_item_2_id}"),
                                                                    data_bag_item_3_id => api_url("/data/#{data_bag_name}/#{data_bag_item_3_id}"),
                                                                  },
                                                                })
          end
        end
      end # /data/bag/item
    end # that have items
  end # with data bags
end # describe
