#
# Author:: John Keiser (<jkeiser@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 "knife_spec_helper"
require "support/shared/integration/integration_helper"
require "chef/knife/upload"
require "chef/knife/diff"
require "chef/knife/raw"
require "chef/json_compat"

describe "knife upload", :workstation do
  include IntegrationSupport
  include KnifeSupport

  context "without versioned cookbooks" do

    when_the_chef_server "has one of each thing" do

      before do
        client "x", {}
        cookbook "x", "1.0.0"
        data_bag "x", { "y" => {} }
        environment "x", {}
        node "x", {}
        role "x", {}
        user "x", {}
      end

      when_the_repository "has only top-level directories" do
        before do
          directory "clients"
          directory "cookbooks"
          directory "data_bags"
          directory "environments"
          directory "nodes"
          directory "roles"
          directory "users"
        end

        it "knife upload does nothing" do
          knife("upload /").should_succeed ""
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients/chef-validator.json
            D\t/clients/chef-webui.json
            D\t/clients/x.json
            D\t/cookbooks/x
            D\t/data_bags/x
            D\t/environments/_default.json
            D\t/environments/x.json
            D\t/nodes/x.json
            D\t/roles/x.json
            D\t/users/admin.json
            D\t/users/x.json
          EOM
        end

        it "knife upload --purge deletes everything" do
          knife("upload --purge /").should_succeed(<<~EOM, stderr: "WARNING: /environments/_default.json cannot be deleted (default environment cannot be modified).\n")
            Deleted extra entry /clients/chef-validator.json (purge is on)
            Deleted extra entry /clients/chef-webui.json (purge is on)
            Deleted extra entry /clients/x.json (purge is on)
            Deleted extra entry /cookbooks/x (purge is on)
            Deleted extra entry /data_bags/x (purge is on)
            Deleted extra entry /environments/x.json (purge is on)
            Deleted extra entry /nodes/x.json (purge is on)
            Deleted extra entry /roles/x.json (purge is on)
            Deleted extra entry /users/admin.json (purge is on)
            Deleted extra entry /users/x.json (purge is on)
          EOM
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/environments/_default.json
          EOM
        end
      end

      when_the_repository "has an identical copy of each thing" do

        before do
          file "clients/chef-validator.json", { "validator" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "clients/chef-webui.json", { "admin" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "clients/x.json", { "public_key" => ChefZero::PUBLIC_KEY }
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
          file "data_bags/x/y.json", {}
          file "environments/_default.json", { "description" => "The default Chef environment" }
          file "environments/x.json", {}
          file "nodes/x.json", { "normal" => { "tags" => [] } }
          file "roles/x.json", {}
          file "users/admin.json", { "admin" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "users/x.json", { "public_key" => ChefZero::PUBLIC_KEY }
        end

        it "knife upload makes no changes" do
          knife("upload /cookbooks/x").should_succeed ""
          knife("diff --name-status /").should_succeed ""
        end

        it "knife upload --purge makes no changes" do
          knife("upload --purge /").should_succeed ""
          knife("diff --name-status /").should_succeed ""
        end

        context "except the role file" do
          before do
            file "roles/x.json", { "description" => "blarghle" }
          end

          it "knife upload changes the role" do
            knife("upload /").should_succeed "Updated /roles/x.json\n"
            knife("diff --name-status /").should_succeed ""
          end
          it "knife upload --no-diff does not change the role" do
            knife("upload --no-diff /").should_succeed ""
            knife("diff --name-status /").should_succeed "M\t/roles/x.json\n"
          end
        end

        context "except the role file is textually different, but not ACTUALLY different" do
          before do
            file "roles/x.json", <<~EOM
              {
                "chef_type": "role",
                "default_attributes":  {
                },
                "env_run_lists": {
                },
                "json_class": "Chef::Role",
                "name": "x",
                "description": "",
                "override_attributes": {
                },
                "run_list": [

                ]
              }
            EOM
          end

          it "knife upload / does not change anything" do
            knife("upload /").should_succeed ""
            knife("diff --name-status /").should_succeed ""
          end
        end

        context "the role is in ruby" do
          before do
            file "roles/x.rb", <<~EOM
              name "x"
              description "blargle"
            EOM
          end

          it "knife upload changes the role" do
            knife("upload /").should_succeed "Updated /roles/x.json\n"
            knife("diff --name-status /").should_succeed ""
          end

          it "knife upload --no-diff does not change the role" do
            knife("upload --no-diff /").should_succeed ""
            knife("diff --name-status /").should_succeed "M\t/roles/x.rb\n"
          end
        end

        context "when cookbook metadata has a self-dependency" do
          before do
            file "cookbooks/x/metadata.rb", "name 'x'; version '1.0.0'; depends 'x'"
          end

          it "fails with RuntimeError" do
            expect { knife("upload /cookbooks") }.to raise_error RuntimeError, /Cookbook depends on itself/
          end
        end

        context "as well as one extra copy of each thing" do
          before do
            file "clients/y.json", { "public_key" => ChefZero::PUBLIC_KEY }
            file "cookbooks/x/blah.rb", ""
            file "cookbooks/y/metadata.rb", cb_metadata("y", "1.0.0")
            file "data_bags/x/z.json", {}
            file "data_bags/y/zz.json", {}
            file "environments/y.json", {}
            file "nodes/y.json", {}
            file "roles/y.json", {}
            file "users/y.json", { "public_key" => ChefZero::PUBLIC_KEY }
          end

          it "knife upload adds the new files" do
            knife("upload /").should_succeed <<~EOM
              Created /clients/y.json
              Updated /cookbooks/x
              Created /cookbooks/y
              Created /data_bags/x/z.json
              Created /data_bags/y
              Created /data_bags/y/zz.json
              Created /environments/y.json
              Created /nodes/y.json
              Created /roles/y.json
              Created /users/y.json
            EOM
            knife("diff --name-status /").should_succeed <<~EOM
              D\t/cookbooks/x/metadata.json
              D\t/cookbooks/y/metadata.json
            EOM
          end

          it "knife upload --no-diff adds the new files" do
            knife("upload --no-diff /").should_succeed <<~EOM
              Created /clients/y.json
              Updated /cookbooks/x
              Created /cookbooks/y
              Created /data_bags/x/z.json
              Created /data_bags/y
              Created /data_bags/y/zz.json
              Created /environments/y.json
              Created /nodes/y.json
              Created /roles/y.json
              Created /users/y.json
            EOM
            knife("diff --name-status /").should_succeed <<~EOM
              D\t/cookbooks/x/metadata.json
              D\t/cookbooks/y/metadata.json
            EOM
          end
        end
      end

      when_the_repository "is empty" do
        it "knife upload does nothing" do
          knife("upload /").should_succeed ""
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients
            D\t/cookbooks
            D\t/data_bags
            D\t/environments
            D\t/nodes
            D\t/roles
            D\t/users
          EOM
        end

        it "knife upload --purge deletes nothing" do
          knife("upload --purge /").should_fail <<~EOM
            ERROR: /clients cannot be deleted.
            ERROR: /cookbooks cannot be deleted.
            ERROR: /data_bags cannot be deleted.
            ERROR: /environments cannot be deleted.
            ERROR: /nodes cannot be deleted.
            ERROR: /roles cannot be deleted.
            ERROR: /users cannot be deleted.
          EOM
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients
            D\t/cookbooks
            D\t/data_bags
            D\t/environments
            D\t/nodes
            D\t/roles
            D\t/users
          EOM
        end

        context "when current directory is top level" do
          before do
            cwd "."
          end

          it "knife upload with no parameters reports an error" do
            knife("upload").should_fail "FATAL: You must specify at least one argument. If you want to upload everything in this directory, run \"knife upload .\"\n", stdout: /USAGE/
          end
        end
      end
    end

    when_the_chef_server "is empty" do
      when_the_repository "has a data bag item" do

        before do
          file "data_bags/x/y.json", { "foo" => "bar" }
        end

        it "knife upload of the data bag uploads only the values in the data bag item and no other" do
          knife("upload /data_bags/x/y.json").should_succeed <<~EOM
            Created /data_bags/x
            Created /data_bags/x/y.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
          EOM
          expect(Chef::JSONCompat.parse(knife("raw /data/x/y").stdout, create_additions: false).keys.sort).to eq(%w{foo id})
        end

        it "knife upload /data_bags/x /data_bags/x/y.json uploads x once" do
          knife("upload /data_bags/x /data_bags/x/y.json").should_succeed <<~EOM
            Created /data_bags/x
            Created /data_bags/x/y.json
          EOM
        end
      end

      when_the_repository "has a data bag item with keys chef_type and data_bag" do

        before do
          file "data_bags/x/y.json", { "chef_type" => "aaa", "data_bag" => "bbb" }
        end

        it "upload preserves chef_type and data_bag" do
          knife("upload /data_bags/x/y.json").should_succeed <<~EOM
            Created /data_bags/x
            Created /data_bags/x/y.json
          EOM
          knife("diff --name-status /data_bags").should_succeed ""
          result = Chef::JSONCompat.parse(knife("raw /data/x/y").stdout, create_additions: false)
          expect(result.keys.sort).to eq(%w{chef_type data_bag id})
          expect(result["chef_type"]).to eq("aaa")
          expect(result["data_bag"]).to eq("bbb")
        end
      end

      # Test upload of an item when the other end doesn't even have the container
      when_the_repository "has two data bag items" do
        before do
          file "data_bags/x/y.json", {}
          file "data_bags/x/z.json", {}
        end
        it "knife upload of one data bag item itself succeeds" do
          knife("upload /data_bags/x/y.json").should_succeed <<~EOM
            Created /data_bags/x
            Created /data_bags/x/y.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            A\t/data_bags/x/z.json
          EOM
        end
      end
    end

    when_the_chef_server "has three data bag items" do

      before do
        data_bag "x", { "deleted" => {}, "modified" => {}, "unmodified" => {} }
      end

      when_the_repository "has a modified, unmodified, added and deleted data bag item" do
        before do
          file "data_bags/x/added.json", {}
          file "data_bags/x/modified.json", { "foo" => "bar" }
          file "data_bags/x/unmodified.json", {}
        end

        it "knife upload of the modified file succeeds" do
          knife("upload /data_bags/x/modified.json").should_succeed <<~EOM
            Updated /data_bags/x/modified.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the unmodified file does nothing" do
          knife("upload /data_bags/x/unmodified.json").should_succeed ""
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the added file succeeds" do
          knife("upload /data_bags/x/added.json").should_succeed <<~EOM
            Created /data_bags/x/added.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
          EOM
        end
        it "knife upload of the deleted file does nothing" do
          knife("upload /data_bags/x/deleted.json").should_succeed ""
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload --purge of the deleted file deletes it" do
          knife("upload --purge /data_bags/x/deleted.json").should_succeed <<~EOM
            Deleted extra entry /data_bags/x/deleted.json (purge is on)
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the entire data bag uploads everything" do
          knife("upload /data_bags/x").should_succeed <<~EOM
            Created /data_bags/x/added.json
            Updated /data_bags/x/modified.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
          EOM
        end
        it "knife upload --purge of the entire data bag uploads everything" do
          knife("upload --purge /data_bags/x").should_succeed <<~EOM
            Created /data_bags/x/added.json
            Updated /data_bags/x/modified.json
            Deleted extra entry /data_bags/x/deleted.json (purge is on)
          EOM
          knife("diff --name-status /data_bags").should_succeed ""
        end
        context "when cwd is the /data_bags directory" do

          before do
            cwd "data_bags"
          end

          it "knife upload fails" do
            knife("upload").should_fail "FATAL: You must specify at least one argument. If you want to upload everything in this directory, run \"knife upload .\"\n", stdout: /USAGE/
          end

          it "knife upload --purge . uploads everything" do
            knife("upload --purge .").should_succeed <<~EOM
              Created x/added.json
              Updated x/modified.json
              Deleted extra entry x/deleted.json (purge is on)
            EOM
            knife("diff --name-status /data_bags").should_succeed ""
          end
          it "knife upload --purge * uploads everything" do
            knife("upload --purge *").should_succeed <<~EOM
              Created x/added.json
              Updated x/modified.json
              Deleted extra entry x/deleted.json (purge is on)
            EOM
            knife("diff --name-status /data_bags").should_succeed ""
          end
        end
      end
    end

    # Cookbook upload is a funny thing ... direct cookbook upload works, but
    # upload of a file is designed not to work at present.  Make sure that is the
    # case.
    when_the_chef_server "has a cookbook" do
      before do
        cookbook "x", "1.0.0", { "z.rb" => "" }
      end

      when_the_repository "does not have metadata file" do
        before do
          file "cookbooks/x/y.rb", "hi"
        end

        it "raises MetadataNotFound exception" do
          expect { knife("upload /cookbooks/x") }.to raise_error(Chef::Exceptions::MetadataNotFound)
        end
      end

      when_the_repository "does not have valid metadata" do
        before do
          file "cookbooks/x/metadata.rb", cb_metadata(nil, "1.0.0")
        end

        it "raises exception for invalid metadata" do
          expect { knife("upload /cookbooks/x") }.to raise_error(Chef::Exceptions::MetadataNotValid)
        end
      end

      when_the_repository "has a modified, extra and missing file for the cookbook" do
        before do
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0", "#modified")
          file "cookbooks/x/y.rb", "hi"
        end

        it "knife upload of any individual file fails" do
          knife("upload /cookbooks/x/metadata.rb").should_fail "ERROR: /cookbooks/x/metadata.rb cannot be updated.\n"
          knife("upload /cookbooks/x/y.rb").should_fail "ERROR: /cookbooks/x cannot have a child created under it.\n"
          knife("upload --purge /cookbooks/x/z.rb").should_fail "ERROR: /cookbooks/x/z.rb cannot be deleted.\n"
        end

        # TODO this is a bit of an inconsistency: if we didn't specify --purge,
        # technically we shouldn't have deleted missing files.  But ... cookbooks
        # are a special case.
        it "knife upload of the cookbook itself succeeds" do
          knife("upload /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end

        it "knife upload --purge of the cookbook itself succeeds" do
          knife("upload /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end
      when_the_repository "has a missing file for the cookbook" do

        before do
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
        end

        it "knife upload of the cookbook succeeds" do
          knife("upload /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end
      when_the_repository "has an extra file for the cookbook" do

        before do
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
          file "cookbooks/x/z.rb", ""
          file "cookbooks/x/blah.rb", ""
        end

        it "knife upload of the cookbook succeeds" do
          knife("upload /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end

      when_the_repository "has a different file in the cookbook" do
        before do
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
        end

        it "knife upload --freeze freezes the cookbook" do
          knife("upload --freeze /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          # Modify a file and attempt to upload
          file "cookbooks/x/metadata.rb", 'name "x"; version "1.0.0"#different'
          knife("upload /cookbooks/x").should_fail "ERROR: /cookbooks failed to write: Cookbook x is frozen\n"
        end
      end
    end

    when_the_chef_server "has a frozen cookbook" do
      before do
        cookbook "frozencook", "1.0.0", {}, frozen: true
      end

      when_the_repository "has an update to said cookbook" do

        before do
          file "cookbooks/frozencook/metadata.rb", cb_metadata("frozencook", "1.0.0", "# This is different")
        end

        it "knife upload fails to upload the frozen cookbook" do
          knife("upload /cookbooks/frozencook").should_fail "ERROR: /cookbooks failed to write: Cookbook frozencook is frozen\n"
        end
        it "knife upload --force uploads the frozen cookbook" do
          knife("upload --force /cookbooks/frozencook").should_succeed <<~EOM
            Updated /cookbooks/frozencook
          EOM
        end
      end
    end

    when_the_repository "has a cookbook" do
      before do
        file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
        file "cookbooks/x/metadata.json", { name: "x", version: "1.0.0" }
        file "cookbooks/x/onlyin1.0.0.rb", "old_text"
      end

      when_the_chef_server "has a later version for the cookbook" do
        before do
          cookbook "x", "1.0.0", { "onlyin1.0.0.rb" => "" }
          cookbook "x", "1.0.1", { "onlyin1.0.1.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the local version" do
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/metadata.json
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
          knife("upload --purge /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/metadata.json
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
        end
      end
    end

    when_the_repository "has a cookbook" do
      before do
        file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
        file "cookbooks/x/onlyin1.0.0.rb", "old_text"
      end

      when_the_chef_server "has a later version for the cookbook" do
        before do
          cookbook "x", "1.0.0", { "onlyin1.0.0.rb" => "" }
          cookbook "x", "1.0.1", { "onlyin1.0.1.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the local version and generates metadata.json from metadata.rb and uploads it." do
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
          knife("upload --purge /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
        end
      end

      when_the_chef_server "has an earlier version for the cookbook" do
        before do
          cookbook "x", "1.0.0", { "onlyin1.0.0.rb" => "" }
          cookbook "x", "0.9.9", { "onlyin0.9.9.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the local version generates metadata.json and uploads it." do
          knife("upload --purge /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end

      when_the_chef_server "has a later version for the cookbook, and no current version" do
        before do
          cookbook "x", "1.0.1", { "onlyin1.0.1.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the local version and generates metadata.json before upload and uploads it." do
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
          knife("upload --purge /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x/metadata.rb
            D\t/cookbooks/x/onlyin1.0.1.rb
            A\t/cookbooks/x/onlyin1.0.0.rb
          EOM
        end
      end

      when_the_chef_server "has an earlier version for the cookbook, and no current version" do
        before do
          cookbook "x", "0.9.9", { "onlyin0.9.9.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the new version" do
          knife("upload --purge /cookbooks/x").should_succeed <<~EOM
            Updated /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end
    end

    when_the_chef_server "has an environment" do
      before do
        environment "x", {}
      end

      when_the_repository "has an environment with bad JSON" do
        before do
          file "environments/x.json", "{"
        end

        it "knife upload tries and fails" do
          error1 = <<~EOH
            WARN: Parse error reading #{path_to("environments/x.json")} as JSON: parse error: premature EOF
                                                   {
                                 (right here) ------^

            ERROR: /environments/x.json failed to write: Parse error reading JSON: parse error: premature EOF
                                                          {
                                        (right here) ------^
          EOH

          warn = <<~EOH
            WARN: Parse error reading #{path_to("environments/x.json")} as JSON: parse error: premature EOF
                                                   {
                                 (right here) ------^

          EOH
          knife("upload /environments/x.json").should_fail(error1)
          knife("diff --name-status /environments/x.json").should_succeed("M\t/environments/x.json\n", stderr: warn)
        end
      end

      when_the_repository "has the same environment with the wrong name in the file" do
        before do
          file "environments/x.json", { "name" => "y" }
        end
        it "knife upload fails" do
          knife("upload /environments/x.json").should_fail "ERROR: /environments/x.json failed to write: Name must be 'x' (is 'y')\n"
          knife("diff --name-status /environments/x.json").should_succeed "M\t/environments/x.json\n"
        end
      end

      when_the_repository "has the same environment with no name in the file" do
        before do
          file "environments/x.json", { "description" => "hi" }
        end
        it "knife upload succeeds" do
          knife("upload /environments/x.json").should_succeed "Updated /environments/x.json\n"
          knife("diff --name-status /environments/x.json").should_succeed ""
        end
      end
    end

    when_the_chef_server "is empty" do

      when_the_repository "has an environment with the wrong name in the file" do
        before do
          file "environments/x.json", { "name" => "y" }
        end
        it "knife upload fails" do
          knife("upload /environments/x.json").should_fail "ERROR: /environments failed to create_child: Error creating 'x.json': Name must be 'x' (is 'y')\n"
          knife("diff --name-status /environments/x.json").should_succeed "A\t/environments/x.json\n"
        end
      end

      when_the_repository "has an environment with no name in the file" do

        before do
          file "environments/x.json", { "description" => "hi" }
        end
        it "knife upload succeeds" do
          knife("upload /environments/x.json").should_succeed "Created /environments/x.json\n"
          knife("diff --name-status /environments/x.json").should_succeed ""
        end
      end

      when_the_repository "has a data bag with no id in the file" do
        before do
          file "data_bags/bag/x.json", { "foo" => "bar" }
        end
        it "knife upload succeeds" do
          knife("upload /data_bags/bag/x.json").should_succeed "Created /data_bags/bag\nCreated /data_bags/bag/x.json\n"
          knife("diff --name-status /data_bags/bag/x.json").should_succeed ""
        end
      end
    end
    when_the_chef_server "is empty" do
      when_the_repository "has a cookbook with an invalid chef_version constraint in it" do
        before do
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0", "\nchef_version '~> 999.0'")
        end
        it "knife upload succeeds" do
          knife("upload /cookbooks/x").should_succeed <<~EOM
            Created /cookbooks/x
          EOM
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x/metadata.json
          EOM
        end
      end
    end
  end # without versioned cookbooks

  context "with versioned cookbooks" do
    before { Chef::Config[:versioned_cookbooks] = true }

    when_the_chef_server "has one of each thing" do

      before do
        client "x", {}
        cookbook "x", "1.0.0"
        data_bag "x", { "y" => {} }
        environment "x", {}
        node "x", {}
        role "x", {}
        user "x", {}
      end

      when_the_repository "has only top-level directories" do
        before do
          directory "clients"
          directory "cookbooks"
          directory "data_bags"
          directory "environments"
          directory "nodes"
          directory "roles"
          directory "users"
        end

        it "knife upload does nothing" do
          knife("upload /").should_succeed ""
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients/chef-validator.json
            D\t/clients/chef-webui.json
            D\t/clients/x.json
            D\t/cookbooks/x-1.0.0
            D\t/data_bags/x
            D\t/environments/_default.json
            D\t/environments/x.json
            D\t/nodes/x.json
            D\t/roles/x.json
            D\t/users/admin.json
            D\t/users/x.json
          EOM
        end

        it "knife upload --purge deletes everything" do
          knife("upload --purge /").should_succeed(<<~EOM, stderr: "WARNING: /environments/_default.json cannot be deleted (default environment cannot be modified).\n")
            Deleted extra entry /clients/chef-validator.json (purge is on)
            Deleted extra entry /clients/chef-webui.json (purge is on)
            Deleted extra entry /clients/x.json (purge is on)
            Deleted extra entry /cookbooks/x-1.0.0 (purge is on)
            Deleted extra entry /data_bags/x (purge is on)
            Deleted extra entry /environments/x.json (purge is on)
            Deleted extra entry /nodes/x.json (purge is on)
            Deleted extra entry /roles/x.json (purge is on)
            Deleted extra entry /users/admin.json (purge is on)
            Deleted extra entry /users/x.json (purge is on)
          EOM
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/environments/_default.json
          EOM
        end
      end

      when_the_repository "has an identical copy of each thing" do
        before do
          file "clients/chef-validator.json", { "validator" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "clients/chef-webui.json", { "admin" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "clients/x.json", { "public_key" => ChefZero::PUBLIC_KEY }
          file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0")
          file "data_bags/x/y.json", {}
          file "environments/_default.json", { "description" => "The default Chef environment" }
          file "environments/x.json", {}
          file "nodes/x.json", { "normal" => { "tags" => [] } }
          file "roles/x.json", {}
          file "users/admin.json", { "admin" => true, "public_key" => ChefZero::PUBLIC_KEY }
          file "users/x.json", { "public_key" => ChefZero::PUBLIC_KEY }
        end

        it "knife upload makes no changes" do
          knife("upload /cookbooks/x-1.0.0").should_succeed ""
          knife("diff --name-status /").should_succeed ""
        end

        it "knife upload --purge makes no changes" do
          knife("upload --purge /").should_succeed ""
          knife("diff --name-status /").should_succeed ""
        end

        context "except the role file" do
          before do
            file "roles/x.json", { "description" => "blarghle" }
          end

          it "knife upload changes the role" do
            knife("upload /").should_succeed "Updated /roles/x.json\n"
            knife("diff --name-status /").should_succeed ""
          end
        end

        context "except the role file is textually different, but not ACTUALLY different" do

          before do
            file "roles/x.json", <<~EOM
              {
                "chef_type": "role",
                "default_attributes":  {
                },
                "env_run_lists": {
                },
                "json_class": "Chef::Role",
                "name": "x",
                "description": "",
                "override_attributes": {
                },
                "run_list": [

                ]
              }
            EOM
          end

          it "knife upload / does not change anything" do
            knife("upload /").should_succeed ""
            knife("diff --name-status /").should_succeed ""
          end
        end

        context "as well as one extra copy of each thing" do
          before do
            file "clients/y.json", { "public_key" => ChefZero::PUBLIC_KEY }
            file "cookbooks/x-1.0.0/blah.rb", ""
            file "cookbooks/x-2.0.0/metadata.rb", cb_metadata("x", "2.0.0")
            file "cookbooks/y-1.0.0/metadata.rb", cb_metadata("y", "1.0.0")
            file "data_bags/x/z.json", {}
            file "data_bags/y/zz.json", {}
            file "environments/y.json", {}
            file "nodes/y.json", {}
            file "roles/y.json", {}
            file "users/y.json", { "public_key" => ChefZero::PUBLIC_KEY }
          end

          it "knife upload adds the new files" do
            knife("upload /").should_succeed <<~EOM
              Created /clients/y.json
              Updated /cookbooks/x-1.0.0
              Created /cookbooks/x-2.0.0
              Created /cookbooks/y-1.0.0
              Created /data_bags/x/z.json
              Created /data_bags/y
              Created /data_bags/y/zz.json
              Created /environments/y.json
              Created /nodes/y.json
              Created /roles/y.json
              Created /users/y.json
            EOM
            knife("diff --name-status /").should_succeed ""
          end
        end
      end

      when_the_repository "is empty" do
        it "knife upload does nothing" do
          knife("upload /").should_succeed ""
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients
            D\t/cookbooks
            D\t/data_bags
            D\t/environments
            D\t/nodes
            D\t/roles
            D\t/users
          EOM
        end

        it "knife upload --purge deletes nothing" do
          knife("upload --purge /").should_fail <<~EOM
            ERROR: /clients cannot be deleted.
            ERROR: /cookbooks cannot be deleted.
            ERROR: /data_bags cannot be deleted.
            ERROR: /environments cannot be deleted.
            ERROR: /nodes cannot be deleted.
            ERROR: /roles cannot be deleted.
            ERROR: /users cannot be deleted.
          EOM
          knife("diff --name-status /").should_succeed <<~EOM
            D\t/clients
            D\t/cookbooks
            D\t/data_bags
            D\t/environments
            D\t/nodes
            D\t/roles
            D\t/users
          EOM
        end

        context "when current directory is top level" do
          before do
            cwd "."
          end
          it "knife upload with no parameters reports an error" do
            knife("upload").should_fail "FATAL: You must specify at least one argument. If you want to upload everything in this directory, run \"knife upload .\"\n", stdout: /USAGE/
          end
        end
      end
    end

    # Test upload of an item when the other end doesn't even have the container
    when_the_chef_server "is empty" do
      when_the_repository "has two data bag items" do
        before do
          file "data_bags/x/y.json", {}
          file "data_bags/x/z.json", {}
        end

        it "knife upload of one data bag item itself succeeds" do
          knife("upload /data_bags/x/y.json").should_succeed <<~EOM
            Created /data_bags/x
            Created /data_bags/x/y.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            A\t/data_bags/x/z.json
          EOM
        end
      end
    end

    when_the_chef_server "has three data bag items" do
      before do
        data_bag "x", { "deleted" => {}, "modified" => {}, "unmodified" => {} }
      end
      when_the_repository "has a modified, unmodified, added and deleted data bag item" do
        before do
          file "data_bags/x/added.json", {}
          file "data_bags/x/modified.json", { "foo" => "bar" }
          file "data_bags/x/unmodified.json", {}
        end

        it "knife upload of the modified file succeeds" do
          knife("upload /data_bags/x/modified.json").should_succeed <<~EOM
            Updated /data_bags/x/modified.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the unmodified file does nothing" do
          knife("upload /data_bags/x/unmodified.json").should_succeed ""
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the added file succeeds" do
          knife("upload /data_bags/x/added.json").should_succeed <<~EOM
            Created /data_bags/x/added.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
          EOM
        end
        it "knife upload of the deleted file does nothing" do
          knife("upload /data_bags/x/deleted.json").should_succeed ""
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload --purge of the deleted file deletes it" do
          knife("upload --purge /data_bags/x/deleted.json").should_succeed <<~EOM
            Deleted extra entry /data_bags/x/deleted.json (purge is on)
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            M\t/data_bags/x/modified.json
            A\t/data_bags/x/added.json
          EOM
        end
        it "knife upload of the entire data bag uploads everything" do
          knife("upload /data_bags/x").should_succeed <<~EOM
            Created /data_bags/x/added.json
            Updated /data_bags/x/modified.json
          EOM
          knife("diff --name-status /data_bags").should_succeed <<~EOM
            D\t/data_bags/x/deleted.json
          EOM
        end
        it "knife upload --purge of the entire data bag uploads everything" do
          knife("upload --purge /data_bags/x").should_succeed <<~EOM
            Created /data_bags/x/added.json
            Updated /data_bags/x/modified.json
            Deleted extra entry /data_bags/x/deleted.json (purge is on)
          EOM
          knife("diff --name-status /data_bags").should_succeed ""
        end
        context "when cwd is the /data_bags directory" do
          before do
            cwd "data_bags"
          end
          it "knife upload fails" do
            knife("upload").should_fail "FATAL: You must specify at least one argument. If you want to upload everything in this directory, run \"knife upload .\"\n", stdout: /USAGE/
          end
          it "knife upload --purge . uploads everything" do
            knife("upload --purge .").should_succeed <<~EOM
              Created x/added.json
              Updated x/modified.json
              Deleted extra entry x/deleted.json (purge is on)
            EOM
            knife("diff --name-status /data_bags").should_succeed ""
          end
          it "knife upload --purge * uploads everything" do
            knife("upload --purge *").should_succeed <<~EOM
              Created x/added.json
              Updated x/modified.json
              Deleted extra entry x/deleted.json (purge is on)
            EOM
            knife("diff --name-status /data_bags").should_succeed ""
          end
        end
      end
    end

    # Cookbook upload is a funny thing ... direct cookbook upload works, but
    # upload of a file is designed not to work at present.  Make sure that is the
    # case.
    when_the_chef_server "has a cookbook" do
      before do
        cookbook "x", "1.0.0", { "z.rb" => "" }
      end

      when_the_repository "has a modified, extra and missing file for the cookbook" do
        before do
          file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0", "#modified")
          file "cookbooks/x-1.0.0/y.rb", "hi"
        end

        it "knife upload of any individual file fails" do
          knife("upload /cookbooks/x-1.0.0/metadata.rb").should_fail "ERROR: /cookbooks/x-1.0.0/metadata.rb cannot be updated.\n"
          knife("upload /cookbooks/x-1.0.0/y.rb").should_fail "ERROR: /cookbooks/x-1.0.0 cannot have a child created under it.\n"
          knife("upload --purge /cookbooks/x-1.0.0/z.rb").should_fail "ERROR: /cookbooks/x-1.0.0/z.rb cannot be deleted.\n"
        end

        # TODO this is a bit of an inconsistency: if we didn't specify --purge,
        # technically we shouldn't have deleted missing files.  But ... cookbooks
        # are a special case.
        it "knife upload of the cookbook itself succeeds" do
          knife("upload /cookbooks/x-1.0.0").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end

        it "knife upload --purge of the cookbook itself succeeds" do
          knife("upload /cookbooks/x-1.0.0").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end

      when_the_repository "has a missing file for the cookbook" do
        before do
          file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0")
        end

        it "knife upload of the cookbook succeeds" do
          knife("upload /cookbooks/x-1.0.0").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end

      when_the_repository "has an extra file for the cookbook" do
        before do
          file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0")
          file "cookbooks/x-1.0.0/z.rb", ""
          file "cookbooks/x-1.0.0/blah.rb", ""
        end

        it "knife upload of the cookbook succeeds" do
          knife("upload /cookbooks/x-1.0.0").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end
    end

    when_the_repository "has a cookbook" do
      before do
        file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0")
        file "cookbooks/x-1.0.0/onlyin1.0.0.rb", "old_text"
      end

      when_the_chef_server "has a later version for the cookbook" do
        before do
          cookbook "x", "1.0.0", { "onlyin1.0.0.rb" => "" }
          cookbook "x", "1.0.1", { "onlyin1.0.1.rb" => "hi" }
        end

        it "knife upload /cookbooks uploads the local version" do
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            M\t/cookbooks/x-1.0.0/onlyin1.0.0.rb
            D\t/cookbooks/x-1.0.1
          EOM
          knife("upload --purge /cookbooks").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
            Deleted extra entry /cookbooks/x-1.0.1 (purge is on)
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end

      when_the_chef_server "has an earlier version for the cookbook" do
        before do
          cookbook "x", "1.0.0", { "onlyin1.0.0.rb" => "" }
          cookbook "x", "0.9.9", { "onlyin0.9.9.rb" => "hi" }
        end
        it "knife upload /cookbooks uploads the local version" do
          knife("upload --purge /cookbooks").should_succeed <<~EOM
            Updated /cookbooks/x-1.0.0
            Deleted extra entry /cookbooks/x-0.9.9 (purge is on)
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end

      when_the_chef_server "has a later version for the cookbook, and no current version" do
        before do
          cookbook "x", "1.0.1", { "onlyin1.0.1.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the local version" do
          knife("diff --name-status /cookbooks").should_succeed <<~EOM
            D\t/cookbooks/x-1.0.1
            A\t/cookbooks/x-1.0.0
          EOM
          knife("upload --purge /cookbooks").should_succeed <<~EOM
            Created /cookbooks/x-1.0.0
            Deleted extra entry /cookbooks/x-1.0.1 (purge is on)
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end

      when_the_chef_server "has an earlier version for the cookbook, and no current version" do
        before do
          cookbook "x", "0.9.9", { "onlyin0.9.9.rb" => "hi" }
        end

        it "knife upload /cookbooks/x uploads the new version" do
          knife("upload --purge /cookbooks").should_succeed <<~EOM
            Created /cookbooks/x-1.0.0
            Deleted extra entry /cookbooks/x-0.9.9 (purge is on)
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end
    end

    when_the_chef_server "has an environment" do
      before do
        environment "x", {}
      end

      when_the_repository "has the same environment with the wrong name in the file" do
        before do
          file "environments/x.json", { "name" => "y" }
        end
        it "knife upload fails" do
          knife("upload /environments/x.json").should_fail "ERROR: /environments/x.json failed to write: Name must be 'x' (is 'y')\n"
          knife("diff --name-status /environments/x.json").should_succeed "M\t/environments/x.json\n"
        end
      end

      when_the_repository "has the same environment with no name in the file" do
        before do
          file "environments/x.json", { "description" => "hi" }
        end
        it "knife upload succeeds" do
          knife("upload /environments/x.json").should_succeed "Updated /environments/x.json\n"
          knife("diff --name-status /environments/x.json").should_succeed ""
        end
      end
    end

    when_the_chef_server "is empty" do

      when_the_repository "has an environment with the wrong name in the file" do
        before do
          file "environments/x.json", { "name" => "y" }
        end
        it "knife upload fails" do
          knife("upload /environments/x.json").should_fail "ERROR: /environments failed to create_child: Error creating 'x.json': Name must be 'x' (is 'y')\n"
          knife("diff --name-status /environments/x.json").should_succeed "A\t/environments/x.json\n"
        end
      end

      when_the_repository "has an environment with no name in the file" do
        before do
          file "environments/x.json", { "description" => "hi" }
        end
        it "knife upload succeeds" do
          knife("upload /environments/x.json").should_succeed "Created /environments/x.json\n"
          knife("diff --name-status /environments/x.json").should_succeed ""
        end
      end

      when_the_repository "has a data bag with no id in the file" do
        before do
          file "data_bags/bag/x.json", { "foo" => "bar" }
        end
        it "knife upload succeeds" do
          knife("upload /data_bags/bag/x.json").should_succeed "Created /data_bags/bag\nCreated /data_bags/bag/x.json\n"
          knife("diff --name-status /data_bags/bag/x.json").should_succeed ""
        end
      end
    end

    when_the_chef_server "is empty" do
      when_the_repository "has a cookbook with an invalid chef_version constraint in it" do
        before do
          file "cookbooks/x-1.0.0/metadata.rb", cb_metadata("x", "1.0.0", "\nchef_version '~> 999.0'")
        end
        it "knife upload succeeds" do
          knife("upload /cookbooks/x-1.0.0").should_succeed <<~EOM
            Created /cookbooks/x-1.0.0
          EOM
          knife("diff --name-status /cookbooks").should_succeed ""
        end
      end
    end
  end # with versioned cookbooks

  when_the_chef_server "has a user" do
    before do
      user "x", {}
    end

    when_the_repository "has the same user with json_class in it" do
      before do
        file "users/x.json", { "admin" => true, "json_class" => "Chef::WebUIUser" }
      end
      it "knife upload /users/x.json succeeds" do
        knife("upload /users/x.json").should_succeed "Updated /users/x.json\n"
      end
    end
  end

  when_the_chef_server "is in Enterprise mode", osc_compat: false, single_org: false do
    before do
      user "foo", {}
      user "bar", {}
      user "foobar", {}
      organization "foo", { "full_name" => "Something" }
    end

    before :each do
      Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo")
    end

    context "and has nothing but a single group named blah" do
      group "blah", {}

      when_the_repository "has at least one of each thing" do

        before do
          # TODO We have to upload acls for an existing group due to a lack of
          # dependency detection during upload.  Fix that!
          file "acls/groups/blah.json", {}
          file "clients/x.json", { "public_key" => ChefZero::PUBLIC_KEY }
          file "containers/x.json", {}
          file "cookbooks/x/metadata.rb", cb_metadata("x", "1.0.0")
          file "cookbook_artifacts/x-1x1/metadata.rb", cb_metadata("x", "1.0.0")
          file "data_bags/x/y.json", {}
          file "environments/x.json", {}
          file "groups/x.json", {}
          file "invitations.json", [ "foo" ]
          file "members.json", [ "bar" ]
          file "org.json", { "full_name" => "wootles" }
          file "nodes/x.json", { "normal" => { "tags" => [] } }
          file "policies/x-1.0.0.json", { "policy_group_list" => [ "x" ] }
          file "policies/blah-1.0.0.json", { "policy_group_list" => [ "x" ] }
          file "policy_groups/x.json", { "policies" => { "x" => { "revision_id" => "1.0.0" }, "blah" => { "revision_id" => "1.0.0" } } }
          file "roles/x.json", {}
        end

        it "knife upload / uploads everything" do
          knife("upload /").should_succeed <<~EOM
            Updated /acls/groups/blah.json
            Created /clients/x.json
            Created /containers/x.json
            Created /cookbook_artifacts/x-1x1
            Created /cookbooks/x
            Created /data_bags/x
            Created /data_bags/x/y.json
            Created /environments/x.json
            Created /groups/x.json
            Updated /invitations.json
            Updated /members.json
            Created /nodes/x.json
            Updated /org.json
            Created /policies/blah-1.0.0.json
            Created /policies/x-1.0.0.json
            Created /policy_groups/x.json
            Created /roles/x.json
          EOM
          expect(api.get("association_requests").map { |a| a["username"] }).to eq([ "foo" ])
          expect(api.get("users").map { |a| a["user"]["username"] }).to eq([ "bar" ])
          knife("diff --name-status --diff-filter=AMT /").should_succeed ""
        end

        context "When the chef server has an identical copy of each thing" do
          before do
            file "invitations.json", [ "foo" ]
            file "members.json", [ "bar" ]
            file "org.json", { "full_name" => "Something" }

            # acl_for %w(organizations foo groups blah)
            client "x", {}
            cookbook "x", "1.0.0"
            cookbook_artifact "x", "1x1", "metadata.rb" => cb_metadata("x", "1.0.0")
            container "x", {}
            data_bag "x", { "y" => {} }
            environment "x", {}
            group "x", {}
            org_invite "foo"
            org_member "bar"
            node "x", {}
            policy "x", "1.0.0", {}
            policy "blah", "1.0.0", {}
            policy_group "x", {
              "policies" => {
                "x" => { "revision_id" => "1.0.0" },
                "blah" => { "revision_id" => "1.0.0" },
              },
            }
            role "x", {}
          end

          it "knife upload makes no changes" do
            knife("upload /").should_succeed <<~EOM
              Updated /acls/groups/blah.json
            EOM
          end
        end

        context "When the chef server has a slightly different copy of the policy revision" do
          before do
            policy "x", "1.0.0", { "run_list" => [ "blah" ] }
          end

          it "should fail because policies are not updateable" do
            knife("upload /policies/x-1.0.0.json").should_fail <<~EOM
              ERROR: /policies/x-1.0.0.json cannot be updated: policy revisions are immutable once uploaded. If you want to change the policy, create a new revision with your changes.
            EOM
          end
        end

        context "When the chef server has a slightly different copy of the cookbook artifact" do
          before do
            cookbook_artifact "x", "1x1", { "recipes" => { "default.rb" => "" } }
          end

          it "should fail because cookbook_artifacts cannot be updated" do
            knife("upload /cookbook_artifacts/x-1x1").should_fail <<~EOM
              ERROR: /cookbook_artifacts/x-1x1 cannot be updated: cookbook artifacts are immutable once uploaded.
            EOM
          end
        end

        context "When the chef server has a slightly different copy of each thing (except policy revisions)" do
          before do
            # acl_for %w(organizations foo groups blah)
            client "x", { "validator" => true }
            container "x", {}
            cookbook "x", "1.0.0", { "recipes" => { "default.rb" => "" } }
            cookbook_artifact "x", "1x1", { "metadata.rb" => cb_metadata("x", "1.0.0") }
            data_bag "x", { "y" => { "a" => "b" } }
            environment "x", { "description" => "foo" }
            group "x", { "groups" => [ "admin" ] }
            node "x", { "run_list" => [ "blah" ] }
            policy "x", "1.0.0", { "policy_group_list" => [ "x" ] }
            policy "y", "1.0.0", { "policy_group_list" => [ "x" ] }
            policy_group "x", {
              "policies" => {
                "x" => { "revision_id" => "1.0.0" },
                "y" => { "revision_id" => "1.0.0" },
              },
            }
            role "x", { "run_list" => [ "blah" ] }
          end

          it "knife upload updates everything" do
            knife("upload /").should_succeed <<~EOM
              Updated /acls/groups/blah.json
              Updated /clients/x.json
              Updated /cookbooks/x
              Updated /data_bags/x/y.json
              Updated /environments/x.json
              Updated /groups/x.json
              Updated /invitations.json
              Updated /members.json
              Updated /nodes/x.json
              Updated /org.json
              Created /policies/blah-1.0.0.json
              Updated /policy_groups/x.json
              Updated /roles/x.json
            EOM
            knife("diff --name-status --diff-filter=AMT /").should_succeed ""
          end
        end
      end

      when_the_repository "has an org.json that does not change full_name" do
        before do
          file "org.json", { "full_name" => "Something" }
        end

        it "knife upload / emits a warning for bar and adds foo and foobar" do
          knife("upload /").should_succeed ""
          expect(api.get("/")["full_name"]).to eq("Something")
        end
      end

      when_the_repository "has an org.json that changes full_name" do
        before do
          file "org.json", { "full_name" => "Something Else" }
        end

        it "knife upload / emits a warning for bar and adds foo and foobar" do
          knife("upload /").should_succeed "Updated /org.json\n"
          expect(api.get("/")["full_name"]).to eq("Something Else")
        end
      end

      context "and has invited foo and bar is already a member" do
        org_invite "foo"
        org_member "bar"

        when_the_repository "wants to invite foo, bar and foobar" do
          before do
            file "invitations.json", %w{foo bar foobar}
          end

          it "knife upload / emits a warning for bar and invites foobar" do
            knife("upload /").should_succeed "Updated /invitations.json\n", stderr: "WARN: Could not invite bar to organization foo: User bar is already in organization foo\n"
            expect(api.get("association_requests").map { |a| a["username"] }).to eq(%w{foo foobar})
            expect(api.get("users").map { |a| a["user"]["username"] }).to eq([ "bar" ])
          end
        end

        when_the_repository "wants to make foo, bar and foobar members" do
          before do
            file "members.json", %w{foo bar foobar}
          end

          it "knife upload / emits a warning for bar and adds foo and foobar" do
            knife("upload /").should_succeed "Updated /members.json\n"
            expect(api.get("association_requests").map { |a| a["username"] }).to eq([ ])
            expect(api.get("users").map { |a| a["user"]["username"] }).to eq(%w{bar foo foobar})
          end
        end

        when_the_repository "wants to invite foo and have bar as a member" do
          before do
            file "invitations.json", [ "foo" ]
            file "members.json", [ "bar" ]
          end

          it "knife upload / does nothing" do
            knife("upload /").should_succeed ""
            expect(api.get("association_requests").map { |a| a["username"] }).to eq([ "foo" ])
            expect(api.get("users").map { |a| a["user"]["username"] }).to eq([ "bar" ])
          end
        end
      end

      context "and has invited bar and foo" do
        org_invite "bar", "foo"

        when_the_repository "wants to invite foo and bar (different order)" do
          before do
            file "invitations.json", %w{foo bar}
          end

          it "knife upload / does nothing" do
            knife("upload /").should_succeed ""
            expect(api.get("association_requests").map { |a| a["username"] }).to eq(%w{bar foo})
            expect(api.get("users").map { |a| a["user"]["username"] }).to eq([ ])
          end
        end
      end

      context "and has already added bar and foo as members of the org" do
        org_member "bar", "foo"

        when_the_repository "wants to add foo and bar (different order)" do
          before do
            file "members.json", %w{foo bar}
          end

          it "knife upload / does nothing" do
            knife("upload /").should_succeed ""
            expect(api.get("association_requests").map { |a| a["username"] }).to eq([ ])
            expect(api.get("users").map { |a| a["user"]["username"] }).to eq(%w{bar foo})
          end
        end
      end
    end
  end
end
