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

require "spec_helper"
require "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)

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

  describe "when discovering the current fs state" do
    before do
      allow(@provider).to receive(:shell_out_compacted!).and_return(OpenStruct.new(stdout: ""))
      allow(::File).to receive(:exist?).with("/etc/fstab").and_return(true)
      allow(::File).to receive(:foreach).with("/etc/fstab")
    end

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

    it "should accept device_type :uuid", :not_supported_on_solaris do
      @status = double(stdout: "/dev/sdz1\n", exitstatus: 1)
      @new_resource.device_type :uuid
      @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
      @stdout_findfs = double("STDOUT", first: "/dev/sdz1")
      expect(@provider).to receive(:shell_out_compacted).with("/sbin/findfs", "UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").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
          expect(@provider.network_device?).to be_truthy
        end

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

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

    it "should not call mountable? with load_current_resource - CHEF-1565" do
      allow(::File).to receive(:exists?).with("/dev/sdz1").and_return false
      expect(@provider).to receive(:mounted?).and_return(true)
      expect(@provider).to receive(:enabled?).and_return(true)
      expect(@provider).not_to receive(:mountable?)
      @provider.load_current_resource
    end

    it "should raise an error if the mount device (uuid) does not exist", :not_supported_on_solaris do
      status = double(stdout: "", exitstatus: 1)
      @new_resource.device_type :uuid
      @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
      expect(@provider).to receive(:shell_out_compacted).with("/sbin/findfs", "UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_return(status)
      expect(::File).to receive(:exists?).with("").and_return(false)
      expect { @provider.load_current_resource(); @provider.mountable? }.to raise_error(Chef::Exceptions::Mount)
    end

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

    %w{tmpfs fuse cgroup vboxsf zfs}.each do |fstype|
      it "does not expect the device to exist for #{fstype}" do
        @new_resource.fstype(fstype)
        @new_resource.device("whatever")
        expect { @provider.load_current_resource(); @provider.mountable? }.not_to raise_error
      end
    end

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

    it "should set mounted true if the mount point is found in the mounts list" do
      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: "/dev/sdz1 on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_truthy
    end

    it "should set mounted false if another mount point beginning with the same path is found in the mounts list" do
      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: "/dev/sdz1 on /tmp/foobar type ext3 (rw)\n"))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_falsey
    end

    it "should set mounted true if the symlink target of the device is found in the mounts list" do
      # expand the target path to correct specs on Windows
      target = ::File.expand_path("/dev/mapper/target")

      allow(::File).to receive(:symlink?).with((@new_resource.device).to_s).and_return(true)
      allow(::File).to receive(:readlink).with((@new_resource.device).to_s).and_return(target)

      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: "#{target} on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_truthy
    end

    it "should set mounted true if the symlink target of the device is relative and is found in the mounts list - CHEF-4957" do
      target = "xsdz1"

      # expand the target path to correct specs on Windows
      absolute_target = ::File.expand_path("/dev/xsdz1")

      allow(::File).to receive(:symlink?).with((@new_resource.device).to_s).and_return(true)
      allow(::File).to receive(:readlink).with((@new_resource.device).to_s).and_return(target)

      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: "#{absolute_target} on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_truthy
    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"

      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: mount))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_truthy
    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"

      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: mount))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_falsey
    end

    it "mounted should be false if the mount point is not found in the mounts list" do
      allow(@provider).to receive(:shell_out!).and_return(OpenStruct.new(stdout: "/dev/sdy1 on /tmp/foo type ext3 (rw)\n"))
      @provider.load_current_resource()
      expect(@provider.current_resource.mounted).to be_falsey
    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"

      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2)

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_truthy
    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"

      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield(fstab1).and_yield(fstab2)

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_truthy
    end

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

      allow(::File).to receive(:symlink?).with((@new_resource.device).to_s).and_return(true)
      allow(::File).to receive(:readlink).with((@new_resource.device).to_s).and_return(target)

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

      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield fstab

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_truthy
    end

    it "should set enabled to true if the symlink target is relative and is in fstab - CHEF-4957" do
      target = "xsdz1"

      allow(::File).to receive(:symlink?).with((@new_resource.device).to_s).and_return(true)
      allow(::File).to receive(:readlink).with((@new_resource.device).to_s).and_return(target)

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

      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield fstab

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_truthy
    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"
      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield fstab

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_falsey
    end

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

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_falsey
    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"
      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield(line_1).and_yield(line_2)

      @provider.load_current_resource
      expect(@provider.current_resource.enabled).to be_falsey
    end

    it "should not mangle the mount options if the device in fstab is a symlink" do
      # expand the target path to correct specs on Windows
      target = "/dev/mapper/target"
      options = "rw,noexec,noauto"

      allow(::File).to receive(:symlink?).with(@new_resource.device).and_return(true)
      allow(::File).to receive(:readlink).with(@new_resource.device).and_return(target)

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

    it "should not mangle the mount options if the symlink target is in fstab" do
      target = ::File.expand_path("/dev/mapper/target")
      options = "rw,noexec,noauto"

      allow(::File).to receive(:symlink?).with(@new_resource.device).and_return(true)
      allow(::File).to receive(:readlink).with(@new_resource.device).and_return(target)

      fstab = "#{target} #{@new_resource.mount_point} #{@new_resource.fstype} #{options} 1 2\n"
      allow(::File).to receive(:foreach).with("/etc/fstab").and_yield fstab
      @provider.load_current_resource
      expect(@provider.current_resource.options).to 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
        expect(@provider).to receive(:shell_out_compacted!).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})
        expect(@provider).to receive(:shell_out_compacted!).with("mount", "-t", "ext3", "-o", "rw,noexec,noauto", "/dev/sdz1", "/tmp/foo")
        @provider.mount_fs()
      end

      it "should mount the filesystem specified by uuid", :not_supported_on_solaris do
        status = double(stdout: "/dev/sdz1\n", exitstatus: 1)
        @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
        @new_resource.device_type :uuid
        allow(@provider).to receive(:shell_out_compacted).with("/sbin/findfs", "UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_return(status)
        @stdout_mock = double("stdout mock")
        allow(@stdout_mock).to receive(:each).and_yield("#{@new_resource.device} on #{@new_resource.mount_point}")
        expect(@provider).to receive(:shell_out_compacted!).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
        allow(@current_resource).to receive(:mounted).and_return(true)
        expect(@provider).not_to 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)
        expect(@provider).to 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)
        expect(@provider).not_to 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)
        expect(@provider).to receive(:shell_out_compacted!).with("mount", "-o", "remount,defaults", @new_resource.mount_point)
        @provider.remount_fs
      end

      it "should use mount -o remount with new mount options if remount is supported" do
        @new_resource.supports({ remount: true })
        options = "rw,noexec,noauto"
        @new_resource.options(%w{rw noexec noauto})
        @current_resource.mounted(true)
        expect(@provider).to receive(:shell_out_compacted!).with("mount", "-o", "remount,rw,noexec,noauto", @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)
        expect(@provider).to receive(:umount_fs)
        expect(@provider).to receive(:sleep).with(1)
        expect(@provider).to receive(:mount_fs)
        @provider.remount_fs()
      end

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

    describe "default_mount_options" do
      it "should return the correct default mount options for Linux" do
        @provider.node.override[:os] = "linux"
        expect(@provider.default_mount_options).to eq("defaults")
      end

      it "should return the correct default mount options for AIX" do
        @provider.node.override[:os] = "aix"
        expect(@provider.default_mount_options).to eq("rw")
      end

      it "should return the correct default mount options for Darwin" do
        @provider.node.override[:os] = "darwin"
        expect(@provider.default_mount_options).to eq("rw")
      end

      it "should return the correct default mount options for FreeBSD" do
        @provider.node.override[:os] = "freebsd"
        expect(@provider.default_mount_options).to eq("rw")
      end
    end

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

        @fstab = StringIO.new
        allow(::File).to receive(:open).with("/etc/fstab", "a").and_yield(@fstab)
        @provider.enable_fs
        expect(@fstab.string).to 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)
        expect(::File).not_to 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
        allow(::File).to receive(:readlines).and_return([])
        expect(::File).to receive(:open).once.with("/etc/fstab", "w").and_yield(@fstab)
        expect(::File).to 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]
        allow(::File).to receive(:readlines).with("/etc/fstab").and_return(@fstab_read)
        @fstab_write = StringIO.new
        allow(::File).to receive(:open).with("/etc/fstab", "w").and_yield(@fstab_write)

        @provider.disable_fs
        expect(@fstab_write.string).to match(Regexp.escape(other_mount))
        expect(@fstab_write.string).not_to 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

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

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

      it "should disable only the last entry if enabled is true" do
        allow(@current_resource).to receive(: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",
                      "/dev/sdz1 /tmp/foobar  ext3  defaults  1 2\n"]

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

        @provider.disable_fs
        expect(fstab_write.string).to eq("/dev/sdz1 /tmp/foo  ext3  defaults  1 2\n" +
          "/dev/sdy1 /tmp/foo  ext3  defaults  1 2\n" +
          "/dev/sdz1 /tmp/foobar  ext3  defaults  1 2\n")
      end

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

        allow(::File).to receive(:readlines).with("/etc/fstab").and_return([])
        expect(::File).not_to receive(:open).and_yield(@fstab)

        @provider.disable_fs
      end
    end

    # the fstab might contain the mount with the device as a device but the resource has a label.
    # we should not create two mount lines, but update the existing one
    # not supported on solaris because it can't cope with a UUID device type
    context "when the device is described differently", :not_supported_on_solaris do
      it "should update the existing line" do
        @current_resource.enabled(true)
        status = double(stdout: "/dev/sdz1\n", exitstatus: 1)
        expect(@provider).to receive(:shell_out_compacted).with("/sbin/findfs", "UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a").and_return(status)

        filesystems = [%q{/dev/sdy1 /tmp/foo  ext3  defaults  1 2},
                      %q{/dev/sdz1 /tmp/foo  ext3  defaults  1 2}].join("\n")
        fstab = StringIO.new filesystems

        fstab_write = StringIO.new

        allow(::File).to receive(:readlines).with("/etc/fstab").and_return(fstab.readlines)
        allow(::File).to receive(:open).with("/etc/fstab", "w").and_yield(fstab_write)
        allow(::File).to receive(:open).with("/etc/fstab", "a").and_yield(fstab_write)

        @new_resource.device_type :uuid
        @new_resource.device "d21afe51-a0fe-4dc6-9152-ac733763ae0a"
        @new_resource.dump 1

        @provider.enable_fs
        expect(fstab_write.string).to match(%r{/dev/sdy1\s+/tmp/foo\s+ext3\s+defaults\s+1\s+2})
        expect(fstab_write.string).to match(%r{UUID=d21afe51-a0fe-4dc6-9152-ac733763ae0a\s+/tmp/foo\s+ext3\s+defaults\s+1\s+2})
        expect(fstab_write.string).not_to match(%r{/dev/sdz1\s+/tmp/foo\s+ext3\s+defaults\s+1\s+2})
      end
    end
  end
end