require 'spec_helper'

require 'engineyard-serverside/slug/source/updater'

module EY
  module Serverside
    module Slug
      module Source

        describe Updater do
          let(:source_ref) {'i_am_a_reference'}
          let(:source_opts) {{}}
          let(:source_cache) {Object.new}
          let(:source_uri) {'https://whatever'}
          let(:source) {Object.new}
          let(:servers) {[]}
          let(:deploy_key) {"/path/to/deploy/key"}
          let(:ssh_wrapper) {"/path/to/ssh/wrapper"}
          let(:paths) {Object.new}
          let(:config) {Object.new}
          let(:shell) {Object.new}

          let(:input) {
            {:servers => servers, :config => config, :shell => shell}
          }

          let(:updater) {described_class.new(input)}

          before(:each) do
            allow(config).to receive(:source).and_return(source)
            allow(config).to receive(:paths).and_return(paths)
            allow(source).to receive(:uri).and_return(source_uri)
            allow(source).to receive(:source_cache).and_return(source_cache)
            allow(source).to receive(:opts).and_return(source_opts)
            allow(source).to receive(:ref).and_return(source_ref)
            allow(paths).to receive(:ssh_wrapper).and_return(ssh_wrapper)
            allow(paths).to receive(:deploy_key).and_return(deploy_key)
            allow(source_cache).to receive(:mkpath)
            allow(source_cache).to receive(:to_s).and_return('source_cache')
          end

          it 'is a Railway' do
            expect(updater).to be_a(Railway)
          end

          it 'has the exact steps for updating a source cache' do
            steps = described_class.steps.map {|s| s[:name]}

            expect(steps).to eql(
              [
                :create_source_cache,
                :ensure_ssh_wrapper,
                :determine_if_clone_needed,
                :clone_if_necessary,
                :prune_source_cache,
                :fetch_updates,
                :clean_local_branch,
                :calculate_requested_revision,
                :checkout_requested_revision,
                :sync_submodules,
                :update_submodules,
                :clean_source_cache
              ]
            )
          end

          describe '#update' do
            let(:update) {updater.update}

            it 'calls the railway' do
              expect(updater).to receive(:call).with(input)

              update
            end
          end

          describe '#create_source_cache' do
            let(:create_source_cache) {
              updater.send(:create_source_cache, input)
            }

            it 'creates the source cache on the file system' do
              expect(source_cache).to receive(:mkpath)

              create_source_cache
            end

            context 'when all is good' do
              it 'is a success' do
                expect(create_source_cache.success?).to eql(true)
              end
            end

            context 'when the mkdir fails' do
              before(:each) do
                allow(source_cache).to receive(:mkpath).and_raise("any error")
              end

              it 'is a failure' do
                expect(create_source_cache.failure?).to eql(true)
              end

              it 'has an error regarding the source cache creation failure' do
                expect(create_source_cache.error[:error]).to eql("Could not create #{source_cache}")
              end
            end
          end

          describe '#ensure_ssh_wrapper' do
            let(:wrapper) {updater.send(:ensure_ssh_wrapper, input)}

            context 'when the wrapper already exists' do
              before(:each) do
                allow(File).
                  to receive(:executable?).
                  with(ssh_wrapper).
                  and_return(true)
              end

              it 'is a success' do
                expect(wrapper).to be_a(Result::Success)
              end

              it 'does not modify its input' do
                expect(wrapper.value).to eql(input)
              end
            end

            context 'when the wrapper does not exist' do
              let(:wrapper_file) {Object.new}

              before(:each) do
                allow(File).
                  to receive(:executable?).
                  with(ssh_wrapper).
                  and_return(false)

                allow(File).
                  to receive(:open).
                  with(ssh_wrapper, 'w', 0700).
                  and_return(wrapper_file)

                allow(wrapper_file).
                  to receive(:write)

                allow(wrapper_file).
                  to receive(:close)
              end

              it 'writes the wrapper file' do
                expect(File).
                  to receive(:open).
                  with(ssh_wrapper, 'w', 0700).
                  and_return(wrapper_file)

                expect(wrapper_file).
                  to receive(:write)

                expect(wrapper_file).
                  to receive(:close)

                wrapper
              end

              context 'but writing the file raises an error' do
                let(:onobro) {StandardError.new('onobro')}
                before(:each) do
                  allow(wrapper_file).
                    to receive(:write).
                    and_raise(onobro)
                end

                it 'is a failure' do
                  expect(wrapper).to be_a(Result::Failure)
                end

                it 'records the error' do
                  expect(wrapper.error[:error]).to eql(onobro)
                end
              end

              context 'and writing the file succeeds' do
                it 'is a success' do
                  expect(wrapper).to be_a(Result::Success)
                end

                it 'does not modify its input' do
                  expect(wrapper.value).to eql(input)
                end
              end
            end

          end

          describe '#determine_if_clone_needed' do
            let(:determine) {updater.send(:determine_if_clone_needed, input)}

            before(:each) do
              allow(source_cache).to receive(:directory?).and_return(true)
              allow(updater).to receive(:run_and_output).and_return(source_uri)
            end

            it 'is always a success' do
              expect(determine.success?).to eql(true)
            end

            context 'when the source cache is not a directory' do
              before(:each) do
                allow(source_cache).to receive(:directory?).and_return(false)
              end

              it 'records that a clone is needed' do
                expect(determine.value[:clone_needed]).to eql(true)
              end
            end

            context 'when the source cache is a directory' do
              context 'but its remotes do not include the source URI' do
                before(:each) do
                  allow(updater).to receive(:run_and_output).and_return('')
                end

                it 'records that a clone is needed' do
                  expect(determine.value[:clone_needed]).to eql(true)
                end
              end

              context 'and its remotes include the source URI' do
                it 'records that a clone is not needed' do
                  expect(determine.value[:clone_needed]).to eql(false)
                end
              end
            end

          end

          describe '#clone_if_necessary' do
            let(:clone_input) {input}
            let(:clone_if_necessary) {
              updater.send(:clone_if_necessary, clone_input)
            }

            before(:each) do
              allow(updater).to receive(:run_and_success?).and_return(true)
            end

            it 'is a result' do
              expect(clone_if_necessary).to be_a(Result::Base)
            end

            context 'when no clone is needed' do
              let(:clone_input) {input.merge(:clone_needed => false)}

              it 'is skips cloning' do
                expect(updater).not_to receive(:run_and_success?)

                clone_if_necessary
              end

              it 'is a success' do
                expect(clone_if_necessary.success?).to eql(true)
              end

              it 'does not alter its input' do
                expect(clone_if_necessary.value).to eql(clone_input)
              end
            end

            context 'when a clone is needed' do
              let(:clone_input) {input.merge(:clone_needed => true)}

              it 'clones the repo' do
                expect(updater).to receive(:run_and_success?).and_return(true)

                clone_if_necessary
              end

              context 'but cloning fails' do
                before(:each) do
                  allow(updater).to receive(:run_and_success?).and_return(false)
                end

                it 'is a failure' do
                  expect(clone_if_necessary.failure?).to eql(true)
                end

                it 'contains an error regarding the clone failure' do
                  expect(clone_if_necessary.error[:error]).
                    to eql("Could not clone #{source_uri} to #{source_cache}")
                end
              end

              context 'and cloning succeeds' do
                it 'is a success' do
                  expect(clone_if_necessary.success?).to eql(true)
                end

                it 'does not modify its input' do
                  expect(clone_if_necessary.value).to eql(clone_input)
                end
              end
            end

          end

          describe '#prune_source_cache' do
            let(:prune) {updater.send(:prune_source_cache, input)}

            before(:each) do
              allow(updater).to receive(:run_and_success?).and_return(true)
            end

            it 'is a result' do
              expect(prune).to be_a(Result::Base)
            end

            context 'when pruning fails' do
              before(:each) do
                allow(updater).to receive(:run_and_success?).and_return(false)
              end

              it 'is a failure' do
                expect(prune.failure?).to eql(true)
              end

              it 'contains an error regarding the prune failure' do
                expect(prune.error[:error]).
                  to eql("Could not prune #{source_cache}")
              end
            end

            context 'when pruning succeeds' do
              it 'is a success' do
                expect(prune.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(prune.value).to eql(input)
              end
            end
          end

          describe '#fetch_updates' do
            let(:fetch) {updater.send(:fetch_updates, input)}

            before(:each) do
              allow(updater).to receive(:run_and_success?).and_return(true)
            end

            it 'is a result' do
              expect(fetch).to be_a(Result::Base)
            end

            context 'when fetch fails' do
              before(:each) do
                allow(updater).to receive(:run_and_success?).and_return(false)
              end

              it 'is a failure' do
                expect(fetch.failure?).to eql(true)
              end

              it 'includes an error regarding the fetch failure' do
                expect(fetch.error[:error]).
                  to eql("Could not fetch #{source_cache}")
              end
            end

            context 'when fetch succeeds' do
              it 'is a success' do
                expect(fetch.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(fetch.value).to eql(input)
              end
            end
          end

          describe '#clean_local_branch' do
            let(:clean_local_branch) {updater.send(:clean_local_branch, input)}

            before(:each) do
              allow(updater).to receive(:run_and_success?)
            end

            it 'is always a success' do
              expect(clean_local_branch.success?).to eql(true)
            end

            it 'does not modify its input' do
              expect(clean_local_branch.value).to eql(input)
            end

            it 'spawns a shell to clean the branch' do
              git = updater.git
              ref = updater.ref

              expect(updater).
                to receive(:run_and_success?).
                with("#{git} show-branch #{ref} > /dev/null 2>&1 && #{git} branch -D #{ref} > /dev/null 2>&1")

              clean_local_branch
            end
          end

          describe '#calculate_requested_revision' do
            let(:ref) {updater.ref}
            let(:git) {updater.git}
            let(:calculate) {updater.send(:calculate_requested_revision, input)}

            before(:each) do
              allow(Dir).to receive(:chdir).and_yield
              allow(updater).to receive(:run_and_success?).and_return(true)
            end

            it 'is always a success' do
              expect(calculate.success?).to eql(true)
            end

            it 'checks for the remote branch in the source cache' do
              expect(Dir).to receive(:chdir).and_yield
              expect(updater).
                to receive(:run_and_success?).
                with("#{git} show-branch origin/#{ref} > /dev/null 2>&1")

              calculate
            end

            context 'when the remote branch is found' do
              it 'records the remote branch as the requested branch' do
                expect(calculate.value[:requested_branch]).
                  to eql("origin/#{ref}")
              end
            end

            context 'when the remote branch is not found' do
              before(:each) do
                allow(updater).to receive(:run_and_success?).and_return(false)
              end

              it 'records the raw reference as the requested_branch' do
                expect(calculate.value[:requested_branch]).
                  to eql(ref)
              end
            end
          end

          describe '#checkout_requested_revision' do
            let(:ref) {updater.ref}
            let(:git) {updater.git}
            let(:checkout_input) {input.merge(:requested_branch => ref)}
            let(:checkout) {
              updater.send(:checkout_requested_revision, checkout_input)
            }

            before(:each) do
              allow(Dir).to receive(:chdir).and_yield
              allow(updater).to receive(:run_and_success?).and_return(false)
            end

            it 'is a result' do
              expect(checkout).to be_a(Result::Base)
            end

            context 'when the raw checkout command fails' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with("git checkout --force --quiet '#{ref}'").
                  and_return(false)
              end

              it 'tries to run the raw reset command' do
                expect(updater).
                  to receive(:run_and_success?).
                  with("git reset --hard --quiet '#{ref}'")

                checkout
              end

              context 'and the raw reset command fails' do
                before(:each) do
                  allow(updater).
                    to receive(:run_and_success?).
                    with("git reset --hard --quiet '#{ref}'").
                    and_return(false)
                end

                it 'is a failure' do
                  expect(checkout.failure?).to eql(true)
                end

                it 'contains an error regarding the checkout failure' do
                  expect(checkout.error[:error]).
                    to eql("Could not check out #{ref}")
                end
              end

              context 'but the raw reset command succeeds' do
                before(:each) do
                  allow(updater).
                    to receive(:run_and_success?).
                    with("git reset --hard --quiet '#{ref}'").
                    and_return(true)
                end

                it 'is a success' do
                  expect(checkout.success?).to eql(true)
                end

                it 'does not modify its input' do
                  expect(checkout.value).to eql(checkout_input)
                end
              end
            end

            context 'when the raw checkout command succeeds' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with("git checkout --force --quiet '#{ref}'").
                  and_return(true)
              end

              it 'skips the raw reset' do
                expect(updater).
                  not_to receive(:run_and_success?).
                  with("git reset --hard --quiet '#{ref}'")

                checkout
              end

              it 'is a success' do
                expect(checkout.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(checkout.value).to eql(checkout_input)
              end
            end

          end

          describe '#sync_submodules' do
            let(:ref) {updater.ref}
            let(:git) {updater.git}
            let(:cmd) {"GIT_SSH=#{ssh_wrapper} git submodule sync"}
            let(:sync) {
              updater.send(:sync_submodules, input)
            }

            before(:each) do
              allow(Dir).to receive(:chdir).and_yield
              allow(updater).
                to receive(:run_and_success?).
                with(cmd).
                and_return(false)
            end

            context 'when the sync command fails' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(false)
              end

              it 'is a failure' do
                expect(sync.failure?).to eql(true)
              end

              it 'includes an error regarding the sync failure' do
                expect(sync.error[:error]).to eql('Could not sync submodules')
              end
            end

            context 'when the sync succeeds' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(true)
              end

              it 'is a success' do
                expect(sync.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(sync.value).to eql(input)
              end
            end

          end

          describe '#update_submodules' do
            let(:ref) {updater.ref}
            let(:git) {updater.git}
            let(:cmd) {"GIT_SSH=#{ssh_wrapper} git submodule update --init --recursive"}
            let(:update_submodules) {
              updater.send(:update_submodules, input)
            }

            before(:each) do
              allow(Dir).to receive(:chdir).and_yield
              allow(updater).
                to receive(:run_and_success?).
                with(cmd).
                and_return(false)
            end

            context 'when the update command fails' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(false)
              end

              it 'is a failure' do
                expect(update_submodules.failure?).to eql(true)
              end

              it 'includes an error regarding the sync failure' do
                expect(update_submodules.error[:error]).
                  to eql('Could not update submodules')
              end
            end

            context 'when the update succeeds' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(true)
              end

              it 'is a success' do
                expect(update_submodules.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(update_submodules.value).to eql(input)
              end
            end

          end

          describe '#clean_source_cache' do
            let(:ref) {updater.ref}
            let(:git) {updater.git}
            let(:cmd) {"git clean -dfq"}

            let(:clean) {
              updater.send(:clean_source_cache, input)
            }

            before(:each) do
              allow(Dir).to receive(:chdir).and_yield
              allow(updater).
                to receive(:run_and_success?).
                with(cmd).
                and_return(false)
            end

            context 'when the clean command fails' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(false)
              end

              it 'is a failure' do
                expect(clean.failure?).to eql(true)
              end

              it 'includes an error regarding the clean failure' do
                expect(clean.error[:error]).
                  to eql('Could not clean source')
              end
            end

            context 'when the clean succeeds' do
              before(:each) do
                allow(updater).
                  to receive(:run_and_success?).
                  with(cmd).
                  and_return(true)
              end

              it 'is a success' do
                expect(clean.success?).to eql(true)
              end

              it 'does not modify its input' do
                expect(clean.value).to eql(input)
              end
            end

          end


        end

      end
    end
  end
end
