#
# Author:: Joshua Timberman (<joshua@opscode.com>)
# Copyright:: Copyright (c) 2008 OpsCode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require 'spec_helper'
require 'ostruct'

describe Chef::Provider::Mount::Mount do
  before(:each) do
    @node = Chef::Node.new
    @events = Chef::EventDispatch::Dispatcher.new
    @run_context = Chef::RunContext.new(@node, {}, @events)

    @new_resource = Chef::Resource::Mount.new("/tmp/foo")
    @new_resource.device      "/dev/sdz1"
    @new_resource.device_type :device
    @new_resource.fstype      "ext3"

    @new_resource.supports :remount => false

    @provider = Chef::Provider::Mount::Mount.new(@new_resource, @run_context)

    ::File.stub(:exists?).with("/dev/sdz1").and_return true
    ::File.stub(:exists?).with("/tmp/foo").and_return true
    ::File.stub(:realpath).with("/dev/sdz1").and_return "/dev/sdz1"
    ::File.stub(:realpath).with("/tmp/foo").and_return "/tmp/foo"
  end

  describe "when discovering the current fs state" do
    before do
      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => ''))
      ::File.stub(:foreach).with("/etc/fstab")
    end

    it "should create a current resource with the same mount point and device" do
      @provider.load_current_resource
      @provider.current_resource.name.should == '/tmp/foo'
      @provider.current_resource.mount_point.should == '/tmp/foo'
      @provider.current_resource.device.should == '/dev/sdz1'
    end

    it "should accecpt device_type :uuid" do
      @new_resource.device_type :uuid
      @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
      @stdout_findfs = double("STDOUT", :first => "/dev/sdz1")
      @provider.should_receive(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,@stdout_findfs,@stderr).and_return(@status)
      @provider.load_current_resource()
      @provider.mountable?
    end

    describe "when dealing with network mounts" do
      { "nfs" => "nfsserver:/vol/path",
        "cifs" => "//cifsserver/share" }.each do |type, fs_spec|
        it "should detect network fs_spec (#{type})" do
          @new_resource.device fs_spec
          @provider.network_device?.should be_true
        end

        it "should ignore trailing slash and set mounted to true for network mount (#{type})" do
          @new_resource.device fs_spec
          @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => "#{fs_spec}/ on /tmp/foo type #{type} (rw)\n"))
          @provider.load_current_resource
          @provider.current_resource.mounted.should be_true
        end
      end
    end

    it "should raise an error if the mount device does not exist" do
      ::File.stub(:exists?).with("/dev/sdz1").and_return false
      lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount)
    end

    it "should not call mountable? with load_current_resource - CHEF-1565" do
      ::File.stub(:exists?).with("/dev/sdz1").and_return false
      @provider.should_receive(:mounted?).and_return(true)
      @provider.should_receive(:enabled?).and_return(true)
      @provider.should_not_receive(:mountable?)
      @provider.load_current_resource
    end

    it "should raise an error if the mount device (uuid) does not exist" do
      @new_resource.device_type :uuid
      @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
      status_findfs = double("Status", :exitstatus => 1)
      stdout_findfs = double("STDOUT", :first => nil)
      @provider.should_receive(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,stdout_findfs,@stderr).and_return(status_findfs)
      ::File.should_receive(:exists?).with("").and_return(false)
      lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount)
    end

    it "should raise an error if the mount point does not exist" do
      ::File.stub(:exists?).with("/tmp/foo").and_return false
      lambda { @provider.load_current_resource();@provider.mountable? }.should raise_error(Chef::Exceptions::Mount)
    end

    it "does not expect the device to exist for tmpfs" do
      @new_resource.fstype("tmpfs")
      @new_resource.device("whatever")
      lambda { @provider.load_current_resource();@provider.mountable? }.should_not raise_error
    end

    it "does not expect the device to exist for Fuse filesystems" do
      @new_resource.fstype("fuse")
      @new_resource.device("nilfs#xxx")
      lambda { @provider.load_current_resource();@provider.mountable? }.should_not raise_error
    end

    it "does not expect the device to exist if it's none" do
      @new_resource.device("none")
      lambda { @provider.load_current_resource();@provider.mountable? }.should_not raise_error
    end

    it "should set mounted true if the mount point is found in the mounts list" do
      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => '/dev/sdz1 on /tmp/foo'))
      @provider.load_current_resource()
      @provider.current_resource.mounted.should be_true
    end

    it "should set mounted true if the symlink target of the device is found in the mounts list" do
      target = "/dev/mapper/target"

      ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true)
      ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target)

      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/mapper/target on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      @provider.current_resource.mounted.should be_true
    end

    it "should set mounted true if the mount point is found last in the mounts list" do
      mount = "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n"
      mount << "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n"

      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => mount))
      @provider.load_current_resource()
      @provider.current_resource.mounted.should be_true
    end

    it "should set mounted false if the mount point is not last in the mounts list" do
      mount = "#{@new_resource.device} on #{@new_resource.mount_point} type ext3 (rw)\n"
      mount << "/dev/sdy1 on #{@new_resource.mount_point} type ext3 (rw)\n"

      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => mount))
      @provider.load_current_resource()
      @provider.current_resource.mounted.should be_false
    end

    it "mounted should be false if the mount point is not found in the mounts list" do
      @provider.stub(:shell_out!).and_return(OpenStruct.new(:stdout => "/dev/sdy1 on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      @provider.current_resource.mounted.should be_false
    end

    it "should set enabled to true if the mount point is last in fstab" do
      fstab1 = "/dev/sdy1  /tmp/foo  ext3  defaults  1 2\n"
      fstab2 = "#{@new_resource.device} #{@new_resource.mount_point}  ext3  defaults  1 2\n"

      ::File.stub(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2)

      @provider.load_current_resource
      @provider.current_resource.enabled.should be_true
    end

    it "should set enabled to true if the mount point is not last in fstab and mount_point is a substring of another mount" do
      fstab1 = "#{@new_resource.device} #{@new_resource.mount_point}  ext3  defaults  1 2\n"
      fstab2 = "/dev/sdy1  /tmp/foo/bar  ext3  defaults  1 2\n"

      ::File.stub(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2)

      @provider.load_current_resource
      @provider.current_resource.enabled.should be_true
    end

    it "should set enabled to true if the symlink target is in fstab" do
      target = "/dev/mapper/target"

      ::File.stub(:symlink?).with("#{@new_resource.device}").and_return(true)
      ::File.stub(:readlink).with("#{@new_resource.device}").and_return(target)

      fstab = "/dev/sdz1  /tmp/foo ext3  defaults  1 2\n"

      ::File.stub(:foreach).with("/etc/fstab").and_yield fstab

      @provider.load_current_resource
      @provider.current_resource.enabled.should be_true
    end

    it "should set enabled to false if the mount point is not in fstab" do
      fstab = "/dev/sdy1  #{@new_resource.mount_point}  ext3  defaults  1 2\n"
      ::File.stub(:foreach).with("/etc/fstab").and_yield fstab

      @provider.load_current_resource
      @provider.current_resource.enabled.should be_false
    end

    it "should ignore commented lines in fstab " do
       fstab = "\# #{@new_resource.device}  #{@new_resource.mount_point}  ext3  defaults  1 2\n"
       ::File.stub(:foreach).with("/etc/fstab").and_yield fstab

       @provider.load_current_resource
       @provider.current_resource.enabled.should be_false
     end

    it "should set enabled to false if the mount point is not last in fstab" do
      line_1 = "#{@new_resource.device} #{@new_resource.mount_point}  ext3  defaults  1 2\n"
      line_2 = "/dev/sdy1 #{@new_resource.mount_point}  ext3  defaults  1 2\n"
      ::File.stub(:foreach).with("/etc/fstab").and_yield(line_1).and_yield(line_2)

      @provider.load_current_resource
      @provider.current_resource.enabled.should be_false
    end

    it "should not mangle the mount options if the device in fstab is a symlink" do
      target = "/dev/mapper/target"
      options = "rw,noexec,noauto"

      ::File.stub(:symlink?).with(@new_resource.device).and_return(true)
      ::File.stub(:readlink).with(@new_resource.device).and_return(target)

      fstab = "#{@new_resource.device} #{@new_resource.mount_point} #{@new_resource.fstype} #{options} 1 2\n"
      ::File.stub(:foreach).with("/etc/fstab").and_yield fstab
      @provider.load_current_resource
      @provider.current_resource.options.should eq(options.split(','))
    end

    it "should not mangle the mount options if the symlink target is in fstab" do
      target = "/dev/mapper/target"
      options = "rw,noexec,noauto"

      ::File.stub(:symlink?).with(@new_resource.device).and_return(true)
      ::File.stub(:readlink).with(@new_resource.device).and_return(target)

      fstab = "#{target} #{@new_resource.mount_point} #{@new_resource.fstype} #{options} 1 2\n"
      ::File.stub(:foreach).with("/etc/fstab").and_yield fstab
      @provider.load_current_resource
      @provider.current_resource.options.should eq(options.split(','))
    end
  end

  context "after the mount's state has been discovered" do
    before do
      @current_resource = Chef::Resource::Mount.new("/tmp/foo")
      @current_resource.device       "/dev/sdz1"
      @current_resource.device_type  :device
      @current_resource.fstype       "ext3"

      @provider.current_resource = @current_resource
    end

    describe "mount_fs" do
      it "should mount the filesystem if it is not mounted" do
        @provider.should_receive(:shell_out!).with("mount -t ext3 -o defaults /dev/sdz1 /tmp/foo")
        @provider.mount_fs()
      end

      it "should mount the filesystem with options if options were passed" do
        options = "rw,noexec,noauto"
        @new_resource.options(%w{rw noexec noauto})
        @provider.should_receive(:shell_out!).with("mount -t ext3 -o rw,noexec,noauto /dev/sdz1 /tmp/foo")
        @provider.mount_fs()
      end

      it "should mount the filesystem specified by uuid" do
        @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
        @new_resource.device_type :uuid
        @stdout_findfs = double("STDOUT", :first => "/dev/sdz1")
        @provider.stub(:popen4).with("/sbin/findfs UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_yield(@pid,@stdin,@stdout_findfs,@stderr).and_return(@status)
        @stdout_mock = double('stdout mock')
        @stdout_mock.stub(:each).and_yield("#{@new_resource.device} on #{@new_resource.mount_point}")
        @provider.should_receive(:shell_out!).with("mount -t #{@new_resource.fstype} -o defaults -U #{@new_resource.device} #{@new_resource.mount_point}").and_return(@stdout_mock)
        @provider.mount_fs()
      end

      it "should not mount the filesystem if it is mounted" do
        @current_resource.stub(:mounted).and_return(true)
        @provider.should_not_receive(:shell_out!)
        @provider.mount_fs()
      end

    end

    describe "umount_fs" do
      it "should umount the filesystem if it is mounted" do
        @current_resource.mounted(true)
        @provider.should_receive(:shell_out!).with("umount /tmp/foo")
        @provider.umount_fs()
      end

      it "should not umount the filesystem if it is not mounted" do
        @current_resource.mounted(false)
        @provider.should_not_receive(:shell_out!)
        @provider.umount_fs()
      end
    end

    describe "remount_fs" do
      it "should use mount -o remount if remount is supported" do
        @new_resource.supports({:remount => true})
        @current_resource.mounted(true)
        @provider.should_receive(:shell_out!).with("mount -o remount #{@new_resource.mount_point}")
        @provider.remount_fs
      end

      it "should umount and mount if remount is not supported" do
        @new_resource.supports({:remount => false})
        @current_resource.mounted(true)
        @provider.should_receive(:umount_fs)
        @provider.should_receive(:sleep).with(1)
        @provider.should_receive(:mount_fs)
        @provider.remount_fs()
      end

      it "should not try to remount at all if mounted is false" do
        @current_resource.mounted(false)
        @provider.should_not_receive(:shell_out!)
        @provider.should_not_receive(:umount_fs)
        @provider.should_not_receive(:mount_fs)
        @provider.remount_fs()
      end
    end

    describe "when enabling the fs" do
      it "should enable if enabled isn't true" do
        @current_resource.enabled(false)

        @fstab = StringIO.new
        ::File.stub(:open).with("/etc/fstab", "a").and_yield(@fstab)
        @provider.enable_fs
        @fstab.string.should match(%r{^/dev/sdz1\s+/tmp/foo\s+ext3\s+defaults\s+0\s+2\s*$})
      end

      it "should not enable if enabled is true and resources match" do
        @current_resource.enabled(true)
        @current_resource.fstype("ext3")
        @current_resource.options(["defaults"])
        @current_resource.dump(0)
        @current_resource.pass(2)
        ::File.should_not_receive(:open).with("/etc/fstab", "a")

        @provider.enable_fs
      end

      it "should enable if enabled is true and resources do not match" do
        @current_resource.enabled(true)
        @current_resource.fstype("auto")
        @current_resource.options(["defaults"])
        @current_resource.dump(0)
        @current_resource.pass(2)
        @fstab = StringIO.new
        ::File.stub(:readlines).and_return([])
        ::File.should_receive(:open).once.with("/etc/fstab", "w").and_yield(@fstab)
        ::File.should_receive(:open).once.with("/etc/fstab", "a").and_yield(@fstab)

        @provider.enable_fs
      end
    end

    describe "when disabling the fs" do
      it "should disable if enabled is true" do
        @current_resource.enabled(true)

        other_mount = "/dev/sdy1  /tmp/foo  ext3  defaults  1 2\n"
        this_mount = "/dev/sdz1 /tmp/foo  ext3  defaults  1 2\n"

        @fstab_read = [this_mount, other_mount]
        ::File.stub(:readlines).with("/etc/fstab").and_return(@fstab_read)
        @fstab_write = StringIO.new
        ::File.stub(:open).with("/etc/fstab", "w").and_yield(@fstab_write)

        @provider.disable_fs
        @fstab_write.string.should match(Regexp.escape(other_mount))
        @fstab_write.string.should_not match(Regexp.escape(this_mount))
      end

      it "should disable if enabled is true and ignore commented lines" do
        @current_resource.enabled(true)

        fstab_read = [%q{/dev/sdy1 /tmp/foo  ext3  defaults  1 2},
                      %q{/dev/sdz1 /tmp/foo  ext3  defaults  1 2},
                      %q{#/dev/sdz1 /tmp/foo  ext3  defaults  1 2}]
        fstab_write = StringIO.new

        ::File.stub(:readlines).with("/etc/fstab").and_return(fstab_read)
        ::File.stub(:open).with("/etc/fstab", "w").and_yield(fstab_write)

        @provider.disable_fs
        fstab_write.string.should match(%r{^/dev/sdy1 /tmp/foo  ext3  defaults  1 2$})
        fstab_write.string.should match(%r{^#/dev/sdz1 /tmp/foo  ext3  defaults  1 2$})
        fstab_write.string.should_not match(%r{^/dev/sdz1 /tmp/foo  ext3  defaults  1 2$})
      end

      it "should disable only the last entry if enabled is true" do
        @current_resource.stub(:enabled).and_return(true)
        fstab_read = ["/dev/sdz1 /tmp/foo  ext3  defaults  1 2\n",
                      "/dev/sdy1 /tmp/foo  ext3  defaults  1 2\n",
                      "/dev/sdz1 /tmp/foo  ext3  defaults  1 2\n"]

        fstab_write = StringIO.new
        ::File.stub(:readlines).with("/etc/fstab").and_return(fstab_read)
        ::File.stub(:open).with("/etc/fstab", "w").and_yield(fstab_write)

        @provider.disable_fs
        fstab_write.string.should == "/dev/sdz1 /tmp/foo  ext3  defaults  1 2\n/dev/sdy1 /tmp/foo  ext3  defaults  1 2\n"
      end

      it "should not disable if enabled is false" do
        @current_resource.stub(:enabled).and_return(false)

        ::File.stub(:readlines).with("/etc/fstab").and_return([])
        ::File.should_not_receive(:open).and_yield(@fstab)

        @provider.disable_fs
      end
    end
  end
end