require 'spec_helper'

describe HealthReporter do
  subject { HealthReporter }

  it 'has a version number' do
    expect(HealthReporter::VERSION).not_to be nil
  end

  before(:each) do
    subject.healthy_cache_ttl = 60
    subject.unhealthy_cache_ttl = 30
    subject.self_test = lambda{ true }
    subject.class_variable_set(:@@last_check_time, nil)
    subject.class_variable_set(:@@healthy, nil)
    subject.clear_dependencies
    Timecop.return
    reset_lambda_runner_spy
  end

  describe '#self_test' do
    it 'remembers the self-test lambda passed to it' do
      test_lambda = lambda{ 'ab' == 'cd' }
      subject.self_test = test_lambda
      expect(subject.self_test).to be test_lambda
    end

    it 'uses the self-test lambda passed to it' do
      test_lambda = spy_lambda_returning_false
      subject.self_test = test_lambda
      expect(subject.self_test).to be test_lambda
      expect(subject.healthy?).to be false
      expect(spy_lambda_was_run?).to eq true
    end
  end

  describe '#healthy_cache_ttl' do
    it 'remembers the cache ttl when healthy' do
      subject.healthy_cache_ttl = 10
      expect(subject.healthy_cache_ttl).to eq 10
    end
  end

  describe '#unhealthy_cache_ttl' do
    it 'remembers the cache ttl when not healthy' do
      subject.unhealthy_cache_ttl = 5
      expect(subject.unhealthy_cache_ttl).to eq 5
    end
  end

  describe '#register_dependency' do
    it 'remembers when you add a dependency' do
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      expect(subject.dependencies).to eq({
        'https://hardware-store/status' => { :code => 123, :timeout => 1 }
      })
    end

    it 'validates the urls of the dependency during registration' do
      expect{subject.register_dependency(url: 'no-valid-url')}.to raise_error RuntimeError, "Configured URL no-valid-url is invalid"
    end

    it 'adds dependency without removing the dependencies already registered' do
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      subject.register_dependency(url: 'https://grocery-store/status', code: 123, timeout: 1)
      expect(subject.dependencies).to eq({
        'https://hardware-store/status' => { :code => 123, :timeout => 1 },
        'https://grocery-store/status' => { :code => 123, :timeout => 1 }
      })
    end

    it 'does not duplicate similar dependency urls' do
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      subject.register_dependency(url: 'https://hardware-store/status', code: 123, timeout: 1)
      subject.register_dependency(url: 'https://grocery-store/status', code: 123, timeout: 1)
      expect(subject.dependencies).to eq({
        'https://hardware-store/status' => { :code => 123, :timeout => 1 },
        'https://grocery-store/status' => { :code => 123, :timeout => 1 }
      })
    end
  end


  def self.register_dependencies(provided_dependencies = [])
    provided_dependencies.map{ |dependency|
      @@dependencies[url] = dependency.delete(:url)
    }
  end


  describe '#register_dependencies' do
    let(:provided_test_dependencies_with_string_keys) {
      [
        {
          "url" => "https://cake-place/health",
          "code" => 200,
          "timeout" => 3
        },
        {
          "url" => "https://coffee-place/status",
          "code" => 200,
          "timeout" => 3
        },
        {
          "url" => "http://postoffice/status",
          "code" => 200,
          "timeout" => 3
        },
        {
          "url" => "http://postoffice/status",
          "code" => 200,
          "timeout" => 3,
          "authorization_token" => "api_token"
        },
        {
          "url" => "http://duplicated/status",
          "code" => 200,
          "timeout" => 3,
          "authorization_token" => "api_token"
        },
        {
          "url" => "http://duplicated/status",
          "code" => 200,
          "timeout" => 3,
          "authorization_token" => "api_token"
        },
        {
          "url" => "https://pizza-place/health",
          "code" => 204,
          "timeout" => 2,
          "username" => "checker",
          "password" => "checking"
        }
      ]
    }
    let(:provided_test_dependencies_with_symbol_keys) {
      [
        {
          url: "https://cake-place/health",
          code: 200,
          timeout: 3
        },
        {
          url: "https://coffee-place/status",
          code: 200,
          timeout: 3
        },
        {
          url: "http://postoffice/status",
          code: 200,
          timeout: 3
        },
        {
          url: "http://postoffice/status",
          code: 200,
          timeout: 3,
          authorization_token: "api_token"
        },
        {
          url: "http://duplicated/status",
          code: 200,
          timeout: 3,
          authorization_token: "api_token"
        },
        {
          url: "http://duplicated/status",
          code: 200,
          timeout: 3,
          authorization_token: "api_token"
        },
        {
          url: "https://pizza-place/health",
          code: 204,
          timeout: 2,
          username: "checker",
          password: "checking"
        }
      ]
    }
    let(:expected_dependencies) {
      {
        "https://cake-place/health" => {
          code: 200,
          timeout: 3
        },
        "https://coffee-place/status" => {
          code: 200,
          timeout: 3
        },
        "http://postoffice/status" => {
          code: 200,
          timeout: 3,
          authorization_token: "api_token"
        },
        "http://duplicated/status" => {
          code: 200,
          timeout: 3,
          authorization_token: "api_token"
        },
        "https://pizza-place/health" => {
          code: 204,
          timeout: 2,
          username: "checker",
          password: "checking"
        }
      }
    }
    it 'remembers when you add the dependencies with string keys' do
      subject.register_dependencies(provided_test_dependencies_with_string_keys)
      expect(subject.dependencies).to eq(expected_dependencies)
    end
    it 'remembers when you add the dependencies with symbol keys' do
      subject.register_dependencies(provided_test_dependencies_with_symbol_keys)
      expect(subject.dependencies).to eq(expected_dependencies)
    end


  end

  describe '#clear_cache' do
    it 'clears the cache when requested' do
      subject.healthy?
      expect(subject.class_variable_get('@@healthy')).to be true
      expect(subject.class_variable_get('@@last_check_time')).to_not be nil
      subject.clear_cache
      expect(subject.class_variable_get('@@healthy')).to be nil
      expect(subject.class_variable_get('@@last_check_time')).to be nil
    end
  end

  describe '#healthy?' do
    context 'when exercising self-test lambda' do
      it 'allows true to be returned by self-test lambda' do
        test_lambda = lambda{ true }
        subject.self_test = test_lambda
        expect(subject.healthy?).to be true
      end
  
      it 'allows false to be returned by self-test lambda' do
        test_lambda = lambda{ false }
        subject.self_test = test_lambda
        expect(subject.healthy?).to be false
      end
  
      it 'raises an exception when non-boolean values are returned by self-test lambda' do
        test_lambda = lambda{ "I don't feel well..." }
        subject.self_test = test_lambda
        expect{subject.healthy?}.to raise_error RuntimeError, "Invalid non-boolean response from registered self-check lambda: I don't feel well..."
      end
    end
  
    context 'when initialized without any parameters' do
      it 'sets the default self-test lambda to { true }' do
        expect(subject.healthy?).to eq true
      end
  
      it 'sets the default healthy state cache ttl to 60 seconds' do
        expect(subject.healthy_cache_ttl).to eq 60
      end
  
      it 'sets the default unhealthy state cache ttl to 30 seconds' do
        expect(subject.unhealthy_cache_ttl).to eq 30
      end
    end
  
    context 'when calling health check for first time (no cached health state)' do
      it 'calls the configured self-test lambda and returns health' do
        subject.self_test = spy_lambda_returning_false
        expect(subject.healthy?).to be false
        expect(spy_lambda_was_run?).to eq true
      end
    end
  
    context 'when current state is healty' do
      before(:each) do
        subject.self_test = spy_lambda_returning_true
        subject.healthy? #force the self-test
        reset_lambda_runner_spy #reset the spy so that we can see if it was run or not
      end
  
      context 'when neither healty-cache-ttl nor unhealty-cache-ttl has expired' do
        before(:each) do
          subject.unhealthy_cache_ttl = 10
          subject.healthy_cache_ttl   = 10
          Timecop.freeze(Time.now + 5)
        end
  
        it 'does not call the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq false
        end
        it 'returns the current healthy state' do
          expect(subject.healthy?).to be true
        end
      end
  
      context 'when healty-cache-ttl has expired' do
        before do
          subject.unhealthy_cache_ttl = 10
          subject.healthy_cache_ttl   =  3
          Timecop.freeze(Time.now + 5)
        end
  
        it 'calls the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq true
        end
        it 'returns the current healthy state' do
          expect(subject.healthy?).to be true
        end
      end
  
      context 'when unhealty-cache-ttl has expired' do
        before(:each) do
          subject.unhealthy_cache_ttl =  3
          subject.healthy_cache_ttl   = 10
          Timecop.freeze(Time.now + 5)
        end
  
        it 'does not call the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq false
        end
        it 'returns the current healthy state' do
          expect(subject.healthy?).to be true
        end
      end
    end
  
    context 'when current state is unhealty' do
      before(:each) do
        subject.self_test = spy_lambda_returning_false
        subject.healthy? #force the self-test
        reset_lambda_runner_spy #reset the spy so that we can see if it was run or not
      end
  
      context 'when neither healty-cache-ttl nor unhealty-cache-ttl has expired' do
        before(:each) do
          subject.unhealthy_cache_ttl = 10
          subject.healthy_cache_ttl   = 10
          Timecop.freeze(Time.now + 5)
        end
  
        it 'does not call the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq false
        end
        it 'returns the current unhealthy state' do
          expect(subject.healthy?).to be false
        end
      end
  
      context 'when healty-cache-ttl has expired' do
        before(:each) do
          subject.unhealthy_cache_ttl = 10
          subject.healthy_cache_ttl   =  3
          Timecop.freeze(Time.now + 5)
        end
  
        it 'does not call the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq false
        end
        it 'returns the current unhealthy state' do
          expect(subject.healthy?).to be false
        end
      end
  
      context 'when unhealty-cache-ttl has expired' do
        before(:each) do
          subject.unhealthy_cache_ttl =  3
          subject.healthy_cache_ttl   = 10
          Timecop.freeze(Time.now + 5)
        end
  
        it 'calls the registered self-test lambda' do
          subject.healthy? #request here and test if it was run in expect below
          expect(spy_lambda_was_run?).to eq true
        end
        it 'returns the current unhealthy state' do
          expect(subject.healthy?).to be false
        end
      end
    end
  
    context 'when checking dependencies' do
      context 'when there are no dependencies registered' do
        it 'only performs the self-test' do
          subject.self_test = spy_lambda_returning_false
          expect(subject.healthy?).to be false
          expect(spy_lambda_was_run?).to eq true
        end
      end
  
      context 'when there are multiple dependencies registered' do
        before(:each) do
          subject.register_dependency(url: 'https://hardware-store/status')
          subject.register_dependency(url: 'https://grocery-store/status')
          subject.self_test = spy_lambda_returning_true
        end
  
        it 'performs the self-test and checks all dependencies' do
          stub_request(:get, "https://hardware-store/status").to_return(:status => 200, :body => "", :headers => {})
          stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
          expect(subject.healthy?).to be true
          expect(spy_lambda_was_run?).to eq true
        end
  
        it 'indicates healthy if all of the dependencies are healthy' do
          stub_request(:get, "https://hardware-store/status").to_return(:status => 200, :body => "", :headers => {})
          stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
          expect(subject.healthy?).to be true
          expect(spy_lambda_was_run?).to eq true
        end
  
        it 'raises a detailed exception indicating why a dependency was determined to be unhealthy state was uncached' do
          stub_request(:get, "https://hardware-store/status").to_return(:status => 500, :body => "", :headers => {})
          stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
          expect{subject.healthy?}.to raise_error RuntimeError, "Dependency <https://hardware-store/status> failed check due to RuntimeError: Response expected to be 200 but is 500"
        end
  
        it 'indicates cached unhealthy state if it is unhealthy because a dependency was unhealthy' do
          stub_request(:get, "https://hardware-store/status").to_return(:status => 500, :body => "", :headers => {})
          stub_request(:get, "https://grocery-store/status").to_return(:status => 200, :body => "", :headers => {})
          expect{subject.healthy?}.to raise_error RuntimeError, "Dependency <https://hardware-store/status> failed check due to RuntimeError: Response expected to be 200 but is 500"
          reset_lambda_runner_spy
          expect(subject.healthy?).to be false
          expect(spy_lambda_was_run?).to eq false
        end
      end

      context 'when a dependency requires basic auth' do
        let(:test_dependencies_with_basic_auth) {
          [
            {
              url: "https://pizza-place/health",
              code: 204,
              timeout: 2,
              username: "check_user",
              password: "check_password"
            }
          ]
        }

        before(:each) do
          subject.register_dependencies(test_dependencies_with_basic_auth)
        end

        it 'reaches out to the dependency using a basic auth and dependency have good health' do
          stub_request(:get, "https://pizza-place/health").
            with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Basic Y2hlY2tfdXNlcjpjaGVja19wYXNzd29yZA==', 'User-Agent'=>'Faraday v0.15.2'}).
            to_return(:status => 204, :body => "", :headers => {})
          expect(subject.healthy?).to be true
        end

        it 'reaches out to the dependency using a basic auth and dependency does not have good health' do
          stub_request(:get, "https://pizza-place/health").
            with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Basic Y2hlY2tfdXNlcjpjaGVja19wYXNzd29yZA==', 'User-Agent'=>'Faraday v0.15.2'}).
            to_return(:status => 500, :body => "", :headers => {})
          expect{subject.healthy?}.to raise_error RuntimeError, "Dependency <https://pizza-place/health> failed check due to RuntimeError: Response expected to be 204 but is 500"
          expect(subject.healthy?).to be false
        end
      end
    end
  end
end
