#
# Author:: Dreamcat4 (<dreamcat4@gmail.com>)
# 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 "spec_helper"
require "ostruct"

describe Chef::Provider::User::Dscl do
  before do
    allow(ChefUtils).to receive(:windows?) { false }
  end

  let(:shellcmdresult) { Struct.new(:stdout, :stderr, :exitstatus) }

  let(:password) { nil }
  let(:salt) { nil }
  let(:iterations) { nil }

  let (:events) { Chef::EventDispatch::Dispatcher.new }

  let(:node) do
    Chef::Node.new.tap do |node|
      node.automatic["os"] = "darwin"
      node.automatic["platform_version"] = "10.13.0"
    end
  end

  let (:run_context) { Chef::RunContext.new(node, {}, events) }

  let(:new_resource) do
    r = Chef::Resource::User::DsclUser.new("toor", run_context)
    r.password(password)
    r.salt(salt)
    r.iterations(iterations)
    r
  end

  let(:provider) do
    Chef::Provider::User::Dscl.new(new_resource, run_context)
  end

  let(:salted_sha512_password) do
    "0f543f021c63255e64e121a3585601b8ecfedf6d2\
705ddac69e682a33db5dbcdb9b56a2520bc8fff63a\
2ba6b7984c0737ff0b7949455071581f7affcd536d\
402b6cdb097"
  end

  let(:salted_sha512_pbkdf2_password) do
    "c734b6e4787c3727bb35e29fdd92b97c\
1de12df509577a045728255ec7c6c5f5\
c18efa05ed02b682ffa7ebc05119900e\
b1d4880833aa7a190afc13e2bf0936b8\
20123e8c98f0f9bcac2a629d9163caac\
9464a8c234f3919082400b4f939bb77b\
c5adbbac718b7eb99463a7b679571e0f\
1c9fef2ef08d0b9e9c2bcf644eed2ffc"
  end

  let(:salted_sha512_pbkdf2_salt) do
    "2d942d8364a9ccf2b8e5cb7ed1ff58f78\
e29dbfee7f9db58859144d061fd0058"
  end

  let(:salted_sha512_pbkdf2_iterations) do
    25000
  end

  let(:vagrant_sha_512) do
    "6f75d7190441facc34291ebbea1fc756b242d4f\
e9bcff141bccb84f1979e27e539539aa31f9f7dcc92c0cea959\
ea18e18b720e358e7fbe3cfbeaa561456f6ba008937a30"
  end

  let(:vagrant_sha_512_pbkdf2) do
    "12601a90db17cbf\
8ba4808e6382fb0d3b9d8a6c1a190477bf680ab21afb\
6065467136e55cc208a6f74156e3daf20fb13369ef4b\
7bafa047d80359fb46a48a4adccd548ebb33851b093\
47cca84341a7f93a27147343f89fb843fb46c0017d2\
64afa4976baacf941b915bd1ec1ca24c30b3e759e02\
403e02f59fe7ff5938a7636c"
  end

  let(:vagrant_sha_512_pbkdf2_salt) do
    "ee954be472fdc60ddf89484781433993625f006af6ec810c08f49a7e413946a1"
  end

  let(:vagrant_sha_512_pbkdf2_iterations) do
    34482
  end

  describe "when shelling out to dscl" do
    it "should run dscl with the supplied cmd /Path args" do
      shell_return = shellcmdresult.new("stdout", "err", 0)
      expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return)
      expect(provider.run_dscl("cmd", "/Path", "args")).to eq("stdout")
    end

    it "returns an empty string from delete commands" do
      shell_return = shellcmdresult.new("out", "err", 23)
      expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-delete", "/Path", "args").and_return(shell_return)
      expect(provider.run_dscl("delete", "/Path", "args")).to eq("")
    end

    it "should raise an exception for any other command" do
      shell_return = shellcmdresult.new("out", "err", 23)
      expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "arguments").and_return(shell_return)
      expect { provider.run_dscl("cmd", "/Path", "arguments") }.to raise_error(Chef::Exceptions::DsclCommandFailed)
    end

    it "raises an exception when dscl reports 'no such key'" do
      shell_return = shellcmdresult.new("No such key: ", "err", 23)
      expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return)
      expect { provider.run_dscl("cmd", "/Path", "args") }.to raise_error(Chef::Exceptions::DsclCommandFailed)
    end

    it "raises an exception when dscl reports 'eDSRecordNotFound'" do
      shell_return = shellcmdresult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", "err", -14136)
      expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-cmd", "/Path", "args").and_return(shell_return)
      expect { provider.run_dscl("cmd", "/Path", "args") }.to raise_error(Chef::Exceptions::DsclCommandFailed)
    end
  end

  describe "get_free_uid" do
    before do
      expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("\nwheel      200\nstaff      201\nbrahms      500\nchopin      501\n")
    end

    describe "when the system property is set to true" do
      before do
        new_resource.system(true)
      end

      it "should return the first unused uid number on or above 200" do
        expect(provider.get_free_uid).to eq(202)
      end
    end

    it "should return the first unused uid number on or above 500" do
      expect(provider.get_free_uid).to eq(502)
    end

    it "should raise an exception when the search limit is exhausted" do
      search_limit = 1
      expect { provider.get_free_uid(search_limit) }.to raise_error(RuntimeError)
    end
  end

  describe "uid_used?" do
    it "should return false if not given any valid uid number" do
      expect(provider.uid_used?(nil)).to be_falsey
    end

    describe "when called with a user id" do
      before do
        expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("\naj      500\n")
      end

      it "should return true for a used uid number" do
        expect(provider.uid_used?(500)).to be_truthy
      end

      it "should return false for an unused uid number" do
        expect(provider.uid_used?(501)).to be_falsey
      end
    end
  end

  describe "when determining the uid to set" do
    it "raises RequestedUIDUnavailable if the requested uid is already in use" do
      allow(provider).to receive(:uid_used?).and_return(true)
      expect(provider).to receive(:get_free_uid).and_return(501)
      expect { provider.dscl_set_uid }.to raise_error(Chef::Exceptions::RequestedUIDUnavailable)
    end

    it "finds a valid, unused uid when none is specified" do
      expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("")
      expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "UniqueID", 501)
      expect(provider).to receive(:get_free_uid).and_return(501)
      provider.dscl_set_uid
      expect(new_resource.uid).to eq(501)
    end

    it "sets the uid specified in the resource" do
      new_resource.uid(1000)
      expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "UniqueID", 1000).and_return(true)
      expect(provider).to receive(:run_dscl).with("list", "/Users", "uid").and_return("")
      provider.dscl_set_uid
    end
  end

  describe "current_home_exists?" do
    let(:current_resource) do
      new_resource.dup
    end

    before do
      provider.current_resource = current_resource
    end

    it "returns false for nil home dir" do
      current_resource.home nil
      expect(provider.current_home_exists?).to be_falsey
    end

    it "is false for empty string" do
      current_resource.home ""
      expect(provider.current_home_exists?).to be_falsey
    end

    it "is true for existing directory" do
      current_resource.home "/Users/blah"
      allow(::File).to receive(:exist?).with("/Users/blah").and_return(true)
      expect(provider.current_home_exists?).to be_truthy
    end
  end

  describe "when modifying the home directory" do
    let(:current_resource) do
      new_resource.dup
    end

    before do
      new_resource.manage_home true
      new_resource.home("/Users/toor")

      provider.current_resource = current_resource
    end

    it "deletes the home directory when resource#home is nil" do
      new_resource.instance_variable_set(:@home, nil)
      expect(provider).to receive(:run_dscl).with("delete", "/Users/toor", "NFSHomeDirectory").and_return(true)
      provider.dscl_set_home
    end

    it "raises InvalidHomeDirectory when the resource's home directory doesn't look right" do
      new_resource.home("epic-fail")
      expect { provider.dscl_set_home }.to raise_error(Chef::Exceptions::InvalidHomeDirectory)
    end

    it "moves the users home to the new location if it exists and the target location is different" do
      new_resource.manage_home true

      current_home = CHEF_SPEC_DATA + "/old_home_dir"
      current_home_files = [current_home + "/my-dot-emacs", current_home + "/my-dot-vim"]
      current_resource.home(current_home)
      new_resource.gid(23)
      allow(::File).to receive(:exist?).with("/old/home/toor").and_return(true)
      allow(::File).to receive(:exist?).with("/Users/toor").and_return(true)
      allow(::File).to receive(:exist?).with(current_home).and_return(true)

      expect(FileUtils).to receive(:mkdir_p).with("/Users/toor").and_return(true)
      expect(FileUtils).to receive(:rmdir).with(current_home)
      expect(::Dir).to receive(:glob).with("#{CHEF_SPEC_DATA}/old_home_dir/*", ::File::FNM_DOTMATCH).and_return(current_home_files)
      expect(FileUtils).to receive(:mv).with(current_home_files, "/Users/toor", force: true)
      expect(FileUtils).to receive(:chown_R).with("toor", "23", "/Users/toor")

      expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "NFSHomeDirectory", "/Users/toor")
      provider.dscl_set_home
    end

    it "should run createhomedir to create the user's new home folder" do
      expect(provider).to receive(:shell_out_compacted!).with("/usr/sbin/createhomedir", "-c", "-u", "toor")
      provider.ditto_home
    end

    it "creates the user's NFSHomeDirectory and home directory" do
      expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "NFSHomeDirectory", "/Users/toor").and_return(true)
      expect(provider).to receive(:ditto_home)
      provider.dscl_set_home
    end
  end

  describe "resource_requirements" do
    let(:dscl_exists) { true }
    let(:plutil_exists) { true }

    before do
      allow(::File).to receive(:exist?).with("/usr/bin/dscl").and_return(dscl_exists)
      allow(::File).to receive(:exist?).with("/usr/bin/plutil").and_return(plutil_exists)
    end

    def run_requirements
      provider.define_resource_requirements
      provider.action = :create
      provider.process_resource_requirements
    end

    describe "when dscl doesn't exist" do
      let(:dscl_exists) { false }

      it "should raise an error" do
        expect { run_requirements }.to raise_error(Chef::Exceptions::User)
      end
    end

    describe "when plutil doesn't exist" do
      let(:plutil_exists) { false }

      it "should raise an error" do
        expect { run_requirements }.to raise_error(Chef::Exceptions::User)
      end
    end

    describe "when password is SALTED-SHA512" do
      let(:password) { salted_sha512_password }

      it "should raise an error" do
        expect { run_requirements }.to raise_error(Chef::Exceptions::User)
      end
    end

    describe "when password is SALTED-SHA512-PBKDF2" do
      let(:password) { salted_sha512_pbkdf2_password }

      describe "when salt and iteration is not set" do
        it "should raise an error" do
          expect { run_requirements }.to raise_error(Chef::Exceptions::User)
        end
      end

      describe "when salt and iteration is set" do
        let(:salt) { salted_sha512_pbkdf2_salt }
        let(:iterations) { salted_sha512_pbkdf2_iterations }

        it "should not raise an error" do
          expect { run_requirements }.not_to raise_error
        end
      end
    end
  end

  describe "load_current_resource" do
    # set this to any of the user plist files under spec/data
    let(:user_plist_file) { nil }

    before do
      expect(provider).to receive(:shell_out_compacted).with("dscacheutil", "-flushcache")
      expect(provider).to receive(:shell_out_compacted).with("plutil", "-convert", "xml1", "-o", "-", "/var/db/dslocal/nodes/Default/users/toor.plist") do
        if user_plist_file.nil?
          shellcmdresult.new("Can not find the file", "Sorry!!", 1)
        else
          shellcmdresult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.plist.xml")), "", 0)
        end
      end

      unless user_plist_file.nil?
        expect(provider).to receive(:convert_binary_plist_to_xml).and_return(File.read(File.join(CHEF_SPEC_DATA, "mac_users/#{user_plist_file}.shadow.xml")))
      end
    end

    describe "when user is not there" do
      it "shouldn't raise an error" do
        expect { provider.load_current_resource }.not_to raise_error
      end

      it "should set @user_exists" do
        provider.load_current_resource
        expect(provider.instance_variable_get(:@user_exists)).to be_falsey
      end

      it "should set username" do
        provider.load_current_resource
        expect(provider.current_resource.username).to eq("toor")
      end
    end

    describe "when user is there" do
      let(:password) { "something" } # Load password during load_current_resource

      let(:user_plist_file) { "10.9" }

      it "collects the user data correctly" do
        provider.load_current_resource
        expect(provider.current_resource.comment).to eq("vagrant")
        expect(provider.current_resource.uid).to eq("501")
        expect(provider.current_resource.gid).to eq("80")
        expect(provider.current_resource.home).to eq("/Users/vagrant")
        expect(provider.current_resource.shell).to eq("/bin/bash")
        expect(provider.current_resource.password).to eq(vagrant_sha_512_pbkdf2)
        expect(provider.current_resource.salt).to eq(vagrant_sha_512_pbkdf2_salt)
        expect(provider.current_resource.iterations).to eq(vagrant_sha_512_pbkdf2_iterations)
      end

      describe "when a plain password is set that is same" do
        let(:password) { "vagrant" }

        it "diverged_password? should report false" do
          provider.load_current_resource
          expect(provider.diverged_password?).to be_falsey
        end
      end

      describe "when a plain password is set that is different" do
        let(:password) { "not_vagrant" }

        it "diverged_password? should report true" do
          provider.load_current_resource
          expect(provider.diverged_password?).to be_truthy
        end
      end

      describe "when iterations change" do
        let(:password) { vagrant_sha_512_pbkdf2 }
        let(:salt) { vagrant_sha_512_pbkdf2_salt }
        let(:iterations) { 12345 }

        it "diverged_password? should report true" do
          provider.load_current_resource
          expect(provider.diverged_password?).to be_truthy
        end
      end

      describe "when shadow hash changes" do
        let(:password) { salted_sha512_pbkdf2_password }
        let(:salt) { vagrant_sha_512_pbkdf2_salt }
        let(:iterations) { vagrant_sha_512_pbkdf2_iterations }

        it "diverged_password? should report true" do
          provider.load_current_resource
          expect(provider.diverged_password?).to be_truthy
        end
      end

      describe "when salt change" do
        let(:password) { vagrant_sha_512_pbkdf2 }
        let(:salt) { salted_sha512_pbkdf2_salt }
        let(:iterations) { vagrant_sha_512_pbkdf2_iterations }

        it "diverged_password? should report true" do
          provider.load_current_resource
          expect(provider.diverged_password?).to be_truthy
        end
      end

      describe "when salt isn't found" do
        it "diverged_password? should report true" do
          provider.load_current_resource
          provider.current_resource.salt(nil)
          expect(provider.diverged_password?).to be_truthy
        end
      end
    end
  end

  describe "salted_sha512_pbkdf2?" do
    it "should return true when the string is a salted_sha512_pbkdf2 hash" do
      expect(provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password)).to be_truthy
    end

    it "should return false otherwise" do
      expect(provider.salted_sha512_pbkdf2?(salted_sha512_password)).to be_falsey
      expect(provider.salted_sha512_pbkdf2?("any other string")).to be_falsey
    end
  end

  describe "salted_sha512?" do
    it "should return true when the string is a salted_sha512_pbkdf2 hash" do
      expect(provider.salted_sha512_pbkdf2?(salted_sha512_pbkdf2_password)).to be_truthy
    end

    it "should return false otherwise" do
      expect(provider.salted_sha512?(salted_sha512_pbkdf2_password)).to be_falsey
      expect(provider.salted_sha512?("any other string")).to be_falsey
    end
  end

  describe "prepare_password_shadow_info" do
    describe "when the password is plain text" do
      let(:password) { "vagrant" }

      it "password_shadow_info should have salted-sha-512 format" do
        shadow_info = provider.prepare_password_shadow_info
        expect(shadow_info).to have_key("SALTED-SHA512-PBKDF2")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("entropy")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("salt")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("iterations")
        info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*").first
        expect(provider.salted_sha512_pbkdf2?(info)).to be_truthy
      end
    end

    describe "when the password is salted-sha-512" do
      let(:password) { vagrant_sha_512_pbkdf2 }
      let(:iterations) { vagrant_sha_512_pbkdf2_iterations }
      let(:salt) { vagrant_sha_512_pbkdf2_salt }

      it "password_shadow_info should have salted-sha-512 format" do
        shadow_info = provider.prepare_password_shadow_info
        expect(shadow_info).to have_key("SALTED-SHA512-PBKDF2")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("entropy")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("salt")
        expect(shadow_info["SALTED-SHA512-PBKDF2"]).to have_key("iterations")
        info = shadow_info["SALTED-SHA512-PBKDF2"]["entropy"].string.unpack("H*").first
        expect(provider.salted_sha512_pbkdf2?(info)).to be_truthy
        expect(info).to eq(vagrant_sha_512_pbkdf2)
      end
    end
  end

  describe "set_password" do
    before do
      new_resource.password("something")
    end

    it "should sleep and flush the dscl cache before saving the password" do
      expect(provider).to receive(:prepare_password_shadow_info).and_return({})
      mock_shellout = double("Mock::Shellout")
      allow(mock_shellout).to receive(:run_command)
      expect(provider).to receive(:shell_out_compacted).and_return(mock_shellout)
      expect(provider).to receive(:read_user_info)
      expect(provider).to receive(:dscl_set)
      expect(provider).to receive(:sleep).with(3)
      expect(provider).to receive(:save_user_info)
      provider.set_password
    end
  end

  describe "when the user does not yet exist and chef is creating it" do
    context "with a numeric gid" do
      before do
        new_resource.comment "#mockssuck"
        new_resource.gid 1001
      end

      it "creates the user, comment field, sets uid, gid, configures the home directory, sets the shell, and sets the password" do
        expect(provider).to receive :dscl_create_user
        expect(provider).to receive :dscl_create_comment
        expect(provider).to receive :dscl_set_uid
        expect(provider).to receive :dscl_set_gid
        expect(provider).to receive :dscl_set_home
        expect(provider).to receive :dscl_set_shell
        expect(provider).to receive :set_password
        provider.create_user
      end

      it "creates the user and sets the comment field" do
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor").and_return(true)
        provider.dscl_create_user
      end

      it "sets the comment field" do
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "RealName", "#mockssuck").and_return(true)
        provider.dscl_create_comment
      end

      it "sets the comment field to username" do
        new_resource.comment nil
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "RealName", "toor").and_return(true)
        provider.dscl_create_comment
      end

      it "should run run_dscl with create /Users/user PrimaryGroupID to set the users primary group" do
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "PrimaryGroupID", 1001).and_return(true)
        provider.dscl_set_gid
      end

      it "should run run_dscl with create /Users/user UserShell to set the users login shell" do
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "UserShell", "/usr/bin/false").and_return(true)
        provider.dscl_set_shell
      end
    end

    context "with a non-numeric gid" do
      before do
        new_resource.comment "#mockssuck"
        new_resource.gid "newgroup"
      end

      it "should map the group name to a numeric ID when the group exists" do
        expect(provider).to receive(:run_dscl).with("read", "/Groups/newgroup", "PrimaryGroupID").ordered.and_return("PrimaryGroupID: 1001\n")
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "PrimaryGroupID", "1001").ordered.and_return(true)
        provider.dscl_set_gid
      end

      it "should raise an exception when the group does not exist" do
        shell_return = shellcmdresult.new("<dscl_cmd> DS Error: -14136 (eDSRecordNotFound)", "err", -14136)
        expect(provider).to receive(:shell_out_compacted).with("dscl", ".", "-read", "/Groups/newgroup", "PrimaryGroupID").and_return(shell_return)
        expect { provider.dscl_set_gid }.to raise_error(Chef::Exceptions::GroupIDNotFound)
      end
    end

    it "should set group ID to 20 if it's not specified" do
      new_resource.gid nil
      expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "PrimaryGroupID", 20).ordered.and_return(true)
      provider.dscl_set_gid
      expect(new_resource.gid).to eq(20)
    end
  end

  describe "when the user exists and chef is managing it" do
    before do
      current_resource = new_resource.dup
      provider.current_resource = current_resource

      # These are all different from current_resource
      new_resource.username "mud"
      new_resource.uid 2342
      new_resource.gid 2342
      new_resource.home "/Users/death"
      new_resource.password "goaway"
    end

    it "sets the user, comment field, uid, gid, moves the home directory, sets the shell, and sets the password" do
      expect(provider).to receive :dscl_create_user
      expect(provider).to receive :dscl_create_comment
      expect(provider).to receive :dscl_set_uid
      expect(provider).to receive :dscl_set_gid
      expect(provider).to receive :dscl_set_home
      expect(provider).to receive :dscl_set_shell
      expect(provider).to receive :set_password
      provider.create_user
    end
  end

  describe "when changing the gid" do
    before do
      current_resource = new_resource.dup
      provider.current_resource = current_resource

      # This is different from current_resource
      new_resource.gid 2342
    end

    it "sets the gid" do
      expect(provider).to receive :dscl_set_gid
      provider.manage_user
    end
  end

  describe "when the user exists" do
    before do
      expect(provider).to receive(:shell_out_compacted).with("dscacheutil", "-flushcache")
      expect(provider).to receive(:shell_out_compacted).with("plutil", "-convert", "xml1", "-o", "-", "/var/db/dslocal/nodes/Default/users/toor.plist") do
        shellcmdresult.new(File.read(File.join(CHEF_SPEC_DATA, "mac_users/10.9.plist.xml")), "", 0)
      end
      provider.load_current_resource
    end

    describe "when Chef is removing the user" do
      it "removes the user from the groups and deletes home directory when the resource is configured to manage home" do
        new_resource.manage_home true
        expect(provider).to receive(:run_dscl).with("list", "/Groups").and_return("my_group\nyour_group\nreal_group\n")
        expect(provider).to receive(:run_dscl).with("read", "/Groups/my_group").and_raise(Chef::Exceptions::DsclCommandFailed) # Empty group
        expect(provider).to receive(:run_dscl).with("read", "/Groups/your_group").and_return("GroupMembership: not_you")
        expect(provider).to receive(:run_dscl).with("read", "/Groups/real_group").and_return("GroupMembership: toor")
        expect(provider).to receive(:run_dscl).with("delete", "/Groups/real_group", "GroupMembership", "toor")
        expect(provider).to receive(:run_dscl).with("delete", "/Users/toor")
        expect(FileUtils).to receive(:rm_rf).with("/Users/vagrant")
        provider.remove_user
      end
    end

    describe "when user is not locked" do
      it "determines the user as not locked" do
        expect(provider).not_to be_locked
      end
    end

    describe "when user is locked" do
      before do
        auth_authority = provider.instance_variable_get(:@authentication_authority)
        provider.instance_variable_set(:@authentication_authority, auth_authority + ";DisabledUser;")
      end

      it "determines the user as not locked" do
        expect(provider).to be_locked
      end

      it "can unlock the user" do
        expect(provider).to receive(:run_dscl).with("create", "/Users/toor", "AuthenticationAuthority", ";ShadowHash;HASHLIST:<SALTED-SHA512-PBKDF2>")
        provider.unlock_user
      end
    end
  end

  describe "when locking the user" do
    it "should run run_dscl with append /Users/user AuthenticationAuthority ;DisabledUser; to lock the user account" do
      expect(provider).to receive(:run_dscl).with("append", "/Users/toor", "AuthenticationAuthority", ";DisabledUser;")
      provider.lock_user
    end
  end

end