require 'spec_helper'
require 'chef/cookbook/synchronizer'
require 'chef/cookbook_version'

describe Chef::CookbookCacheCleaner do
  describe "when cleaning up unused cookbook components" do

    before do
      @cleaner = Chef::CookbookCacheCleaner.instance
      @cleaner.reset!
    end

    it "removes all files that belong to unused cookbooks" do
    end

    it "removes all files not validated during the chef run" do
      file_cache = mock("Chef::FileCache with files from unused cookbooks")
      unused_template_files = %w{cookbooks/unused/templates/default/foo.conf.erb cookbooks/unused/tempaltes/default/bar.conf.erb}
      valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb}
      @cleaner.mark_file_as_valid('cookbooks/valid1/recipes/default.rb')
      @cleaner.mark_file_as_valid('cookbooks/valid2/recipes/default.rb')
      file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(valid_cached_cb_files + unused_template_files)
      file_cache.should_receive(:delete).with('cookbooks/unused/templates/default/foo.conf.erb')
      file_cache.should_receive(:delete).with('cookbooks/unused/tempaltes/default/bar.conf.erb')
      cookbook_hash = {"valid1"=> {}, "valid2" => {}}
      @cleaner.stub!(:cache).and_return(file_cache)
      @cleaner.cleanup_file_cache
    end

    describe "on chef-solo" do
      before do
        Chef::Config[:solo] = true
      end

      after do
        Chef::Config[:solo] = false
      end

      it "does not remove anything" do
        @cleaner.cache.stub!(:find).and_return(%w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb})
        @cleaner.cache.should_not_receive(:delete)
        @cleaner.cleanup_file_cache
      end

    end

  end
end

describe Chef::CookbookSynchronizer do
  before do
    segments = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ]
    @cookbook_manifest = {}
    @cookbook_a = Chef::CookbookVersion.new("cookbook_a")
    @cookbook_a_manifest = segments.inject({}) {|h, segment| h[segment.to_s] = []; h}
    @cookbook_a_default_recipe = { "path" => "recipes/default.rb",
                                   "url"  => "http://chef.example.com/abc123",
                                   "checksum" => "abc123" }
    @cookbook_a_manifest["recipes"] = [ @cookbook_a_default_recipe ]

    @cookbook_a_default_attrs  = { "path" => "attributes/default.rb",
                                   "url"  => "http://chef.example.com/abc456",
                                   "checksum" => "abc456" }
    @cookbook_a_manifest["attributes"] = [ @cookbook_a_default_attrs ]
    @cookbook_a_manifest["templates"] = [{"path" => "templates/default/apache2.conf.erb", "url" => "http://chef.example.com/ffffff"}]
    @cookbook_a_manifest["files"] = [{"path" => "files/default/megaman.conf", "url" => "http://chef.example.com/megaman.conf"}]
    @cookbook_a.manifest = @cookbook_a_manifest
    @cookbook_manifest["cookbook_a"] = @cookbook_a

    @events = Chef::EventDispatch::Dispatcher.new
    @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_manifest, @events)
  end

  it "lists the cookbook names" do
    @synchronizer.cookbook_names.should == %w[cookbook_a]
  end

  it "lists the cookbook manifests" do
    @synchronizer.cookbooks.should == [@cookbook_a]
  end

  context "when the cache contains unneeded cookbooks" do
    before do
      @file_cache = mock("Chef::FileCache with files from unused cookbooks")
      @valid_cached_cb_files = %w{cookbooks/valid1/recipes/default.rb cookbooks/valid2/recipes/default.rb}
      @obsolete_cb_files = %w{cookbooks/old1/recipes/default.rb cookbooks/old2/recipes/default.rb}

      @cookbook_hash = {"valid1"=> {}, "valid2" => {}}

      @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_hash, @events)
    end

    it "removes unneeded cookbooks" do
      @file_cache.should_receive(:find).with(File.join(%w{cookbooks ** *})).and_return(@valid_cached_cb_files + @obsolete_cb_files)
      @file_cache.should_receive(:delete).with('cookbooks/old1/recipes/default.rb')
      @file_cache.should_receive(:delete).with('cookbooks/old2/recipes/default.rb')
      @synchronizer.stub!(:cache).and_return(@file_cache)
      @synchronizer.clear_obsoleted_cookbooks
    end
  end

  describe "when syncing cookbooks with the server" do
    before do
      # Would rather not stub out methods on the test subject, but setting up
      # the state is a PITA and tests for this behavior are above.
      @synchronizer.stub!(:clear_obsoleted_cookbooks)

      @server_api = mock("Chef::REST (mock)")
      @file_cache = mock("Chef::FileCache (mock)")
      @synchronizer.stub!(:server_api).and_return(@server_api)
      @synchronizer.stub!(:cache).and_return(@file_cache)


      @cookbook_a_default_recipe_tempfile = mock("Tempfile for cookbook_a default.rb recipe",
                                                 :path => "/tmp/cookbook_a_recipes_default_rb")

      @cookbook_a_default_attribute_tempfile = mock("Tempfile for cookbook_a default.rb attr file",
                                                 :path => "/tmp/cookbook_a_attributes_default_rb")

    end

    context "when the cache does not contain the desired files" do
      before do

        # Files are not in the cache:
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/recipes/default.rb").
          and_return(false)
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/attributes/default.rb").
          and_return(false)

        # Fetch and copy default.rb recipe
        @server_api.should_receive(:get_rest).
          with('http://chef.example.com/abc123', true).
          and_return(@cookbook_a_default_recipe_tempfile)
        @file_cache.should_receive(:move_to).
          with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb")
        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/recipes/default.rb", false).
          and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")

        # Fetch and copy default.rb attribute file
        @server_api.should_receive(:get_rest).
          with('http://chef.example.com/abc456', true).
          and_return(@cookbook_a_default_attribute_tempfile)
        @file_cache.should_receive(:move_to).
          with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb")
        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/attributes/default.rb", false).
          and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")
      end

      it "fetches eagerly loaded files" do
        @synchronizer.sync_cookbooks
      end

      it "does not fetch templates or cookbook files" do
        # Implicitly tested in previous test; this test is just for behavior specification.
        @server_api.should_not_receive(:get_rest).
          with('http://chef.example.com/ffffff', true)

        @synchronizer.sync_cookbooks
      end

      context "Chef::Config[:no_lazy_load] is true" do
        before do
          Chef::Config[:no_lazy_load] = true
          @synchronizer = Chef::CookbookSynchronizer.new(@cookbook_manifest, @events)
          @synchronizer.stub!(:server_api).and_return(@server_api)
          @synchronizer.stub!(:cache).and_return(@file_cache)
          @synchronizer.stub!(:clear_obsoleted_cookbooks)

          @cookbook_a_file_default_tempfile = mock("Tempfile for cookbook_a megaman.conf file",
                                                     :path => "/tmp/cookbook_a_file_default_tempfile")
          @cookbook_a_template_default_tempfile = mock("Tempfile for cookbook_a apache.conf.erb template",
                                                     :path => "/tmp/cookbook_a_template_default_tempfile")
        end

        after do
          Chef::Config[:no_lazy_load] = false
        end

        it "fetches templates and cookbook files" do
          @file_cache.should_receive(:has_key?).
            with("cookbooks/cookbook_a/files/default/megaman.conf").
            and_return(false)
          @file_cache.should_receive(:has_key?).
            with("cookbooks/cookbook_a/templates/default/apache2.conf.erb").
            and_return(false)

          @server_api.should_receive(:get_rest).
            with('http://chef.example.com/megaman.conf', true).
            and_return(@cookbook_a_file_default_tempfile)
          @file_cache.should_receive(:move_to).
            with("/tmp/cookbook_a_file_default_tempfile", "cookbooks/cookbook_a/files/default/megaman.conf")
          @file_cache.should_receive(:load).
            with("cookbooks/cookbook_a/files/default/megaman.conf", false).
            and_return("/file-cache/cookbooks/cookbook_a/default/megaman.conf")

          @server_api.should_receive(:get_rest).
            with('http://chef.example.com/ffffff', true).
            and_return(@cookbook_a_template_default_tempfile)
          @file_cache.should_receive(:move_to).
            with("/tmp/cookbook_a_template_default_tempfile", "cookbooks/cookbook_a/templates/default/apache2.conf.erb")
          @file_cache.should_receive(:load).
            with("cookbooks/cookbook_a/templates/default/apache2.conf.erb", false).
            and_return("/file-cache/cookbooks/cookbook_a/templates/default/apache2.conf.erb")

          @synchronizer.sync_cookbooks
        end
      end
    end

    context "when the cache contains outdated files" do
      before do
        # Files are in the cache:
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/recipes/default.rb").
          and_return(true)
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/attributes/default.rb").
          and_return(true)


        # Fetch and copy default.rb recipe
        @server_api.should_receive(:get_rest).
          with('http://chef.example.com/abc123', true).
          and_return(@cookbook_a_default_recipe_tempfile)
        @file_cache.should_receive(:move_to).
          with("/tmp/cookbook_a_recipes_default_rb", "cookbooks/cookbook_a/recipes/default.rb")
        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/recipes/default.rb", false).
          twice.
          and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")

        # Current file has fff000, want abc123
        Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
          with("/file-cache/cookbooks/cookbook_a/recipes/default.rb").
          and_return("fff000")

        # Fetch and copy default.rb attribute file
        @server_api.should_receive(:get_rest).
          with('http://chef.example.com/abc456', true).
          and_return(@cookbook_a_default_attribute_tempfile)
        @file_cache.should_receive(:move_to).
          with("/tmp/cookbook_a_attributes_default_rb", "cookbooks/cookbook_a/attributes/default.rb")
        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/attributes/default.rb", false).
          twice.
          and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")

        # Current file has fff000, want abc456
        Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
          with("/file-cache/cookbooks/cookbook_a/attributes/default.rb").
          and_return("fff000")
      end

      it "updates the outdated files" do
        @synchronizer.sync_cookbooks
      end
    end

    context "when the cache is up to date" do
      before do
        # Files are in the cache:
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/recipes/default.rb").
          and_return(true)
        @file_cache.should_receive(:has_key?).
          with("cookbooks/cookbook_a/attributes/default.rb").
          and_return(true)

        # Current file has abc123, want abc123
        Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
          with("/file-cache/cookbooks/cookbook_a/recipes/default.rb").
          and_return("abc123")

        # Current file has abc456, want abc456
        Chef::CookbookVersion.should_receive(:checksum_cookbook_file).
          with("/file-cache/cookbooks/cookbook_a/attributes/default.rb").
          and_return("abc456")

        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/recipes/default.rb", false).
          twice.
          and_return("/file-cache/cookbooks/cookbook_a/recipes/default.rb")

        @file_cache.should_receive(:load).
          with("cookbooks/cookbook_a/attributes/default.rb", false).
          twice.
          and_return("/file-cache/cookbooks/cookbook_a/attributes/default.rb")
      end

      it "does not update files" do
        @file_cache.should_not_receive(:move_to)
        @server_api.should_not_receive(:get_rest)
        @synchronizer.sync_cookbooks
      end

    end

  end

end

