require "spec_helper"

module Backup
  describe Syncer::RSync::Push do
    before do
      allow_any_instance_of(Syncer::RSync::Push).to \
        receive(:utility).with(:rsync).and_return("rsync")
      allow_any_instance_of(Syncer::RSync::Push).to \
        receive(:utility).with(:ssh).and_return("ssh")
    end

    describe "#initialize" do
      after { Syncer::RSync::Push.clear_defaults! }

      it "should use the values given" do
        syncer = Syncer::RSync::Push.new("my syncer") do |rsync|
          rsync.mode            = :valid_mode
          rsync.host            = "123.45.678.90"
          rsync.port            = 123
          rsync.ssh_user        = "ssh_username"
          rsync.rsync_user      = "rsync_username"
          rsync.rsync_password  = "rsync_password"
          rsync.rsync_password_file = "/my/rsync_password"
          rsync.mirror          = true
          rsync.compress        = true
          rsync.path            = "~/my_backups/"
          rsync.additional_ssh_options = "ssh options"
          rsync.additional_rsync_options = "rsync options"

          rsync.directories do |directory|
            directory.add "/some/directory/"
            directory.add "~/home/directory"
            directory.exclude "*~"
            directory.exclude "tmp/"
          end
        end

        expect(syncer.syncer_id).to eq "my syncer"
        expect(syncer.mode).to eq :valid_mode
        expect(syncer.host).to eq "123.45.678.90"
        expect(syncer.port).to be 123
        expect(syncer.ssh_user).to eq "ssh_username"
        expect(syncer.rsync_user).to eq "rsync_username"
        expect(syncer.rsync_password).to eq "rsync_password"
        expect(syncer.rsync_password_file).to eq "/my/rsync_password"
        expect(syncer.mirror).to be true
        expect(syncer.compress).to be true
        expect(syncer.path).to eq "~/my_backups/"
        expect(syncer.additional_ssh_options).to eq "ssh options"
        expect(syncer.additional_rsync_options).to eq "rsync options"
        expect(syncer.directories).to eq ["/some/directory/", "~/home/directory"]
        expect(syncer.excludes).to eq ["*~", "tmp/"]
      end

      it "should use default values if none are given" do
        syncer = Syncer::RSync::Push.new

        expect(syncer.syncer_id).to be_nil
        expect(syncer.mode).to eq :ssh
        expect(syncer.host).to be_nil
        expect(syncer.port).to be 22
        expect(syncer.ssh_user).to be_nil
        expect(syncer.rsync_user).to be_nil
        expect(syncer.rsync_password).to be_nil
        expect(syncer.rsync_password_file).to be_nil
        expect(syncer.mirror).to be(false)
        expect(syncer.compress).to be(false)
        expect(syncer.path).to eq "~/backups"
        expect(syncer.additional_ssh_options).to be_nil
        expect(syncer.additional_rsync_options).to be_nil
        expect(syncer.directories).to eq []
        expect(syncer.excludes).to eq []
      end

      it "should use default port 22 for :ssh_daemon mode" do
        syncer = Syncer::RSync::Push.new do |s|
          s.mode = :ssh_daemon
        end
        expect(syncer.mode).to eq :ssh_daemon
        expect(syncer.port).to be 22
      end

      it "should use default port 873 for :rsync_daemon mode" do
        syncer = Syncer::RSync::Push.new do |s|
          s.mode = :rsync_daemon
        end
        expect(syncer.mode).to eq :rsync_daemon
        expect(syncer.port).to be 873
      end

      context "when pre-configured defaults have been set" do
        before do
          Backup::Syncer::RSync::Push.defaults do |rsync|
            rsync.mode            = :default_mode
            rsync.host            = "default_host"
            rsync.port            = 456
            rsync.ssh_user        = "default_ssh_username"
            rsync.rsync_user      = "default_rsync_username"
            rsync.rsync_password  = "default_rsync_password"
            rsync.rsync_password_file = "/my/default_rsync_password"
            rsync.mirror          = true
            rsync.compress        = true
            rsync.path            = "~/default_my_backups"
            rsync.additional_ssh_options = "default ssh options"
            rsync.additional_rsync_options = "default rsync options"
          end
        end

        it "should use pre-configured defaults" do
          syncer = Syncer::RSync::Push.new

          expect(syncer.mode).to eq :default_mode
          expect(syncer.host).to eq "default_host"
          expect(syncer.port).to be 456
          expect(syncer.ssh_user).to eq "default_ssh_username"
          expect(syncer.rsync_user).to eq "default_rsync_username"
          expect(syncer.rsync_password).to eq "default_rsync_password"
          expect(syncer.rsync_password_file).to eq "/my/default_rsync_password"
          expect(syncer.mirror).to be true
          expect(syncer.compress).to be true
          expect(syncer.path).to eq "~/default_my_backups"
          expect(syncer.additional_ssh_options).to eq "default ssh options"
          expect(syncer.additional_rsync_options).to eq "default rsync options"
          expect(syncer.directories).to eq []
        end

        it "should override pre-configured defaults" do
          syncer = Syncer::RSync::Push.new do |rsync|
            rsync.mode            = :valid_mode
            rsync.host            = "123.45.678.90"
            rsync.port            = 123
            rsync.ssh_user        = "ssh_username"
            rsync.rsync_user      = "rsync_username"
            rsync.rsync_password  = "rsync_password"
            rsync.rsync_password_file = "/my/rsync_password"
            rsync.mirror          = true
            rsync.compress        = true
            rsync.path            = "~/my_backups"
            rsync.additional_ssh_options = "ssh options"
            rsync.additional_rsync_options = "rsync options"

            rsync.directories do |directory|
              directory.add "/some/directory"
              directory.add "~/home/directory"
            end
          end

          expect(syncer.mode).to eq :valid_mode
          expect(syncer.host).to eq "123.45.678.90"
          expect(syncer.port).to be 123
          expect(syncer.ssh_user).to eq "ssh_username"
          expect(syncer.rsync_user).to eq "rsync_username"
          expect(syncer.rsync_password).to eq "rsync_password"
          expect(syncer.rsync_password_file).to eq "/my/rsync_password"
          expect(syncer.mirror).to be true
          expect(syncer.compress).to be true
          expect(syncer.path).to eq "~/my_backups"
          expect(syncer.additional_ssh_options).to eq "ssh options"
          expect(syncer.additional_rsync_options).to eq "rsync options"
          expect(syncer.directories).to eq ["/some/directory", "~/home/directory"]
        end
      end # context 'when pre-configured defaults have been set'
    end # describe '#initialize'

    describe "#perform!" do
      # Using :ssh mode, as these are not mode dependant.
      describe "mirror and compress options" do
        specify "with both" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.ssh_user = "ssh_username"
            s.mirror = true
            s.compress = true
            s.path = "~/path/in/remote/home/"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --delete --compress " \
            "-e \"ssh -p 22 -l ssh_username\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'path/in/remote/home'"
          )
          syncer.perform!
        end

        specify "without mirror" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.ssh_user = "ssh_username"
            s.compress = true
            s.path = "relative/path/in/remote/home"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --compress " \
            "-e \"ssh -p 22 -l ssh_username\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'relative/path/in/remote/home'"
          )
          syncer.perform!
        end

        specify "without compress" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.mirror = true
            s.path = "/absolute/path/on/remote/"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --delete " \
            "-e \"ssh -p 22\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'/absolute/path/on/remote'"
          )
          syncer.perform!
        end

        specify "without both" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.path = "/absolute/path/on/remote"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive " \
            "-e \"ssh -p 22\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'/absolute/path/on/remote'"
          )
          syncer.perform!
        end
      end # describe 'mirror and compress options'

      describe "additional_rsync_options" do
        specify "given as an Array (with mirror option)" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.mirror = true
            s.additional_rsync_options = ["--opt-a", "--opt-b"]
            s.path = "path/on/remote/"
            s.directories do |dirs|
              dirs.add "/this/dir"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --delete --opt-a --opt-b " \
            "-e \"ssh -p 22\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'path/on/remote'"
          )
          syncer.perform!
        end

        specify "given as a String (without mirror option)" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.additional_rsync_options = "--opt-a --opt-b"
            s.path = "path/on/remote/"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --opt-a --opt-b " \
            "-e \"ssh -p 22\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'path/on/remote'"
          )
          syncer.perform!
        end

        specify "with excludes" do
          syncer = Syncer::RSync::Push.new do |s|
            s.mode = :ssh
            s.host = "my_host"
            s.additional_rsync_options = "--opt-a --opt-b"
            s.path = "path/on/remote/"
            s.directories do |dirs|
              dirs.add "/this/dir/"
              dirs.add "that/dir"
              dirs.exclude "*~"
              dirs.exclude "tmp/"
            end
          end

          expect(syncer).to receive(:create_dest_path!)
          expect(syncer).to receive(:run).with(
            "rsync --archive --exclude='*~' --exclude='tmp/' --opt-a --opt-b " \
            "-e \"ssh -p 22\" " \
            "'/this/dir' '#{File.expand_path("that/dir")}' " \
            "my_host:'path/on/remote'"
          )
          syncer.perform!
        end
      end # describe 'additional_rsync_options'

      describe "rsync password options" do
        let(:s) { sequence "" }
        let(:password_file) { double }

        context "when an rsync_password is given" do
          let(:syncer) do
            Syncer::RSync::Push.new do |syncer|
              syncer.mode = :rsync_daemon
              syncer.host = "my_host"
              syncer.rsync_user = "rsync_username"
              syncer.rsync_password = "my_password"
              syncer.mirror = true
              syncer.compress = true
              syncer.path = "my_module"
              syncer.directories do |dirs|
                dirs.add "/this/dir"
                dirs.add "that/dir"
              end
            end
          end

          before do
            allow(password_file).to receive(:path).and_return("path/to/password_file")
            expect(Tempfile).to receive(:new).ordered
              .with("backup-rsync-password").and_return(password_file)
            expect(password_file).to receive(:write).ordered.with("my_password")
            expect(password_file).to receive(:close).ordered
          end

          it "creates and uses a temp file for the password" do
            expect(syncer).to receive(:run).ordered.with(
              "rsync --archive --delete --compress " \
              "--password-file='#{File.expand_path("path/to/password_file")}' " \
              "--port 873 " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "rsync_username@my_host::'my_module'"
            )

            expect(password_file).to receive(:delete).ordered

            syncer.perform!
          end

          it "ensures tempfile removal" do
            expect(syncer).to receive(:run).ordered.and_raise("error message")

            expect(password_file).to receive(:delete).ordered

            expect do
              syncer.perform!
            end.to raise_error(RuntimeError, "error message")
          end
        end # context 'when an rsync_password is given'

        context "when an rsync_password_file is given" do
          let(:syncer) do
            Syncer::RSync::Push.new do |syncer|
              syncer.mode = :ssh_daemon
              syncer.host = "my_host"
              syncer.ssh_user = "ssh_username"
              syncer.rsync_user = "rsync_username"
              syncer.rsync_password_file = "path/to/my_password"
              syncer.mirror = true
              syncer.compress = true
              syncer.path = "my_module"
              syncer.directories do |dirs|
                dirs.add "/this/dir"
                dirs.add "that/dir"
              end
            end
          end

          before do
            expect(Tempfile).to receive(:new).never
          end

          it "uses the given path" do
            expect(syncer).to receive(:run).ordered.with(
              "rsync --archive --delete --compress " \
              "--password-file='#{File.expand_path("path/to/my_password")}' " \
              "-e \"ssh -p 22 -l ssh_username\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "rsync_username@my_host::'my_module'"
            )
            syncer.perform!
          end
        end # context 'when an rsync_password_file is given'

        context "when using :ssh mode" do
          let(:syncer) do
            Syncer::RSync::Push.new do |syncer|
              syncer.mode = :ssh
              syncer.host = "my_host"
              syncer.ssh_user = "ssh_username"
              syncer.rsync_user = "rsync_username"
              syncer.rsync_password = "my_password"
              syncer.rsync_password_file = "path/to/my_password"
              syncer.mirror = true
              syncer.compress = true
              syncer.path = "~/path/in/remote/home"
              syncer.directories do |dirs|
                dirs.add "/this/dir"
                dirs.add "that/dir"
              end
            end
          end

          before do
            expect(Tempfile).to receive(:new).never
          end

          it "uses no rsync_user, tempfile or password_option" do
            expect(syncer).to receive(:create_dest_path!)
            expect(syncer).to receive(:run).ordered.with(
              "rsync --archive --delete --compress " \
              "-e \"ssh -p 22 -l ssh_username\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host:'path/in/remote/home'"
            )
            syncer.perform!
          end
        end # context 'when an rsync_password_file is given'
      end # describe 'rsync password options'

      describe "transport_options and host_command" do
        context "using :rsync_daemon mode" do
          it "uses the rsync --port option" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :rsync_daemon
              s.host = "my_host"
              s.mirror = true
              s.compress = true
              s.additional_rsync_options = "--opt-a --opt-b"
              s.path = "module_name/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "rsync --archive --delete --opt-a --opt-b --compress " \
              "--port 873 " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host::'module_name/path'"
            )
            syncer.perform!
          end

          it "uses the rsync_user" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :rsync_daemon
              s.host = "my_host"
              s.port = 789
              s.rsync_user = "rsync_username"
              s.mirror = true
              s.additional_rsync_options = "--opt-a --opt-b"
              s.path = "module_name/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "rsync --archive --delete --opt-a --opt-b " \
              "--port 789 " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "rsync_username@my_host::'module_name/path'"
            )
            syncer.perform!
          end
        end # context 'in :rsync_daemon mode'

        context "using :ssh_daemon mode" do
          specify "rsync_user, additional_ssh_options as an Array" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :ssh_daemon
              s.host = "my_host"
              s.mirror = true
              s.compress = true
              s.additional_ssh_options = ["--opt1", "--opt2"]
              s.rsync_user = "rsync_username"
              s.additional_rsync_options = "--opt-a --opt-b"
              s.path = "module_name/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "rsync --archive --delete --opt-a --opt-b --compress " \
              "-e \"ssh -p 22 --opt1 --opt2\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "rsync_username@my_host::'module_name/path'"
            )
            syncer.perform!
          end

          specify "ssh_user, port, additional_ssh_options as an String" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :ssh_daemon
              s.host = "my_host"
              s.port = 789
              s.mirror = true
              s.compress = true
              s.ssh_user = "ssh_username"
              s.additional_ssh_options = "-i '/my/identity_file'"
              s.additional_rsync_options = "--opt-a --opt-b"
              s.path = "module_name/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "rsync --archive --delete --opt-a --opt-b --compress " \
              "-e \"ssh -p 789 -l ssh_username -i '/my/identity_file'\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host::'module_name/path'"
            )
            syncer.perform!
          end
        end # context 'in :ssh_daemon mode'

        context "using :ssh mode" do
          it "uses no daemon or rsync user" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :ssh
              s.host = "my_host"
              s.mirror = true
              s.compress = true
              s.ssh_user = "ssh_username"
              s.additional_ssh_options = ["--opt1", "--opt2"]
              s.rsync_user = "rsync_username"
              s.additional_rsync_options = "--opt-a 'something'"
              s.path = "~/some/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:create_dest_path!)
            expect(syncer).to receive(:run).with(
              "rsync --archive --delete --opt-a 'something' --compress " \
              "-e \"ssh -p 22 -l ssh_username --opt1 --opt2\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host:'some/path'"
            )
            syncer.perform!
          end
        end # context 'in :ssh mode'
      end # describe 'transport_options and host_command'

      describe "dest_path creation" do
        context "when using :ssh mode" do
          it "creates path using ssh with transport args" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :ssh
              s.host = "my_host"
              s.ssh_user = "ssh_username"
              s.additional_ssh_options = "-i '/path/to/id_rsa'"
              s.path = "~/some/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "ssh -p 22 -l ssh_username -i '/path/to/id_rsa' my_host " +
              %("mkdir -p 'some/path'")
            )

            expect(syncer).to receive(:run).with(
              "rsync --archive " \
              "-e \"ssh -p 22 -l ssh_username -i '/path/to/id_rsa'\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host:'some/path'"
            )

            syncer.perform!
          end

          it "only creates path if mkdir -p is required" do
            syncer = Syncer::RSync::Push.new do |s|
              s.mode = :ssh
              s.host = "my_host"
              s.ssh_user = "ssh_username"
              s.additional_ssh_options = "-i '/path/to/id_rsa'"
              s.path = "~/path/"
              s.directories do |dirs|
                dirs.add "/this/dir/"
                dirs.add "that/dir"
              end
            end

            expect(syncer).to receive(:run).with(
              "rsync --archive " \
              "-e \"ssh -p 22 -l ssh_username -i '/path/to/id_rsa'\" " \
              "'/this/dir' '#{File.expand_path("that/dir")}' " \
              "my_host:'path'"
            )

            syncer.perform!
          end
        end
      end # describe 'dest_path creation'

      describe "logging messages" do
        it "logs started/finished messages" do
          syncer = Syncer::RSync::Push.new

          expect(Logger).to receive(:info).with("Syncer::RSync::Push Started...")
          expect(Logger).to receive(:info).with("Syncer::RSync::Push Finished!")
          syncer.perform!
        end

        it "logs messages using optional syncer_id" do
          syncer = Syncer::RSync::Push.new("My Syncer")

          expect(Logger).to receive(:info).with("Syncer::RSync::Push (My Syncer) Started...")
          expect(Logger).to receive(:info).with("Syncer::RSync::Push (My Syncer) Finished!")
          syncer.perform!
        end
      end
    end # describe '#perform!'
  end
end
