require 'spec_helper'

describe KubernetesService, models: true, caching: true do
  include KubernetesHelpers
  include ReactiveCachingHelpers

  let(:project) { build_stubbed(:kubernetes_project) }
  let(:service) { project.kubernetes_service }

  # We use Kubeclient to interactive with the Kubernetes API. It will
  # GET /api/v1 for a list of resources the API supports. This must be stubbed
  # in addition to any other HTTP requests we expect it to perform.
  let(:discovery_url) { service.api_url + '/api/v1' }
  let(:discovery_response) { { body: kube_discovery_body.to_json } }

  let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods" }
  let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }

  def stub_kubeclient_discover
    WebMock.stub_request(:get, discovery_url).to_return(discovery_response)
  end

  def stub_kubeclient_pods
    stub_kubeclient_discover
    WebMock.stub_request(:get, pods_url).to_return(pods_response)
  end

  describe "Associations" do
    it { is_expected.to belong_to :project }
  end

  describe 'Validations' do
    context 'when service is active' do
      before { subject.active = true }

      it { is_expected.not_to validate_presence_of(:namespace) }
      it { is_expected.to validate_presence_of(:api_url) }
      it { is_expected.to validate_presence_of(:token) }

      context 'namespace format' do
        before do
          subject.project = project
          subject.api_url = "http://example.com"
          subject.token = "test"
        end

        {
          'foo'  => true,
          '1foo' => true,
          'foo1' => true,
          'foo-bar' => true,
          '-foo' => false,
          'foo-' => false,
          'a' * 63 => true,
          'a' * 64 => false,
          'a.b' => false,
          'a*b' => false
        }.each do |namespace, validity|
          it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
            subject.namespace = namespace

            expect(subject.valid?).to eq(validity)
          end
        end
      end
    end

    context 'when service is inactive' do
      before { subject.active = false }

      it { is_expected.not_to validate_presence_of(:api_url) }
      it { is_expected.not_to validate_presence_of(:token) }
    end
  end

  describe '#initialize_properties' do
    context 'without a project' do
      it 'leaves the namespace unset' do
        expect(described_class.new.namespace).to be_nil
      end
    end
  end

  describe '#fields' do
    let(:kube_namespace) do
      subject.fields.find { |h| h[:name] == 'namespace' }
    end

    context 'as template' do
      before { subject.template = true }

      it 'sets the namespace to the default' do
        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:placeholder]).to eq(subject.class::TEMPLATE_PLACEHOLDER)
      end
    end

    context 'with associated project' do
      before { subject.project = project }

      it 'sets the namespace to the default' do
        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
      end
    end
  end

  describe '#actual_namespace' do
    subject { service.actual_namespace }

    it "returns the default namespace" do
      is_expected.to eq(service.send(:default_namespace))
    end
    
    context 'when namespace is specified' do
      before do
        service.namespace = 'my-namespace'
      end

      it "returns the user-namespace" do
        is_expected.to eq('my-namespace')
      end
    end

    context 'when service is not assigned to project' do
      before do
        service.project = nil
      end

      it "does not return namespace" do
        is_expected.to be_nil
      end
    end
  end

  describe '#test' do
    before do
      stub_kubeclient_discover
    end

    context 'with path prefix in api_url' do
      let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }

      it 'tests with the prefix' do
        service.api_url = 'https://kubernetes.example.com/prefix/'

        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'with custom CA certificate' do
      it 'is added to the certificate store' do
        service.ca_pem = "CA PEM DATA"

        cert = double("certificate")
        expect(OpenSSL::X509::Certificate).to receive(:new).with(service.ca_pem).and_return(cert)
        expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)

        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'success' do
      it 'reads the discovery endpoint' do
        expect(service.test[:success]).to be_truthy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end

    context 'failure' do
      let(:discovery_response) { { status: 404 } }

      it 'fails to read the discovery endpoint' do
        expect(service.test[:success]).to be_falsy
        expect(WebMock).to have_requested(:get, discovery_url).once
      end
    end
  end

  describe '#predefined_variables' do
    before do
      subject.api_url = 'https://kube.domain.com'
      subject.token = 'token'
      subject.ca_pem = 'CA PEM DATA'
      subject.project = project
    end

    context 'namespace is provided' do
      before { subject.namespace = 'my-project' }

      it 'sets the variables' do
        expect(subject.predefined_variables).to include(
          { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
          { key: 'KUBE_TOKEN', value: 'token', public: false },
          { key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
          { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
          { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
        )
      end
    end

    context 'no namespace provided' do
      it 'sets the variables' do
        expect(subject.predefined_variables).to include(
          { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
          { key: 'KUBE_TOKEN', value: 'token', public: false },
          { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
          { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
        )
      end

      it 'sets the KUBE_NAMESPACE' do
        kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }

        expect(kube_namespace).not_to be_nil
        expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
      end
    end
  end

  describe '#terminals' do
    let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }

    subject { service.terminals(environment) }

    context 'with invalid pods' do
      it 'returns no terminals' do
        stub_reactive_cache(service, pods: [{ "bad" => "pod" }])

        is_expected.to be_empty
      end
    end

    context 'with valid pods' do
      let(:pod) { kube_pod(app: environment.slug) }
      let(:terminals) { kube_terminals(service, pod) }

      before do
        stub_reactive_cache(
          service,
          pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
        )
      end

      it 'returns terminals' do
        is_expected.to eq(terminals + terminals)
      end

      it 'uses max session time from settings' do
        stub_application_setting(terminal_max_session_time: 600)

        times = subject.map { |terminal| terminal[:max_session_time] }
        expect(times).to eq [600, 600, 600, 600]
      end
    end
  end

  describe '#calculate_reactive_cache' do
    before { stub_kubeclient_pods }
    subject { service.calculate_reactive_cache }

    context 'when service is inactive' do
      before { service.active = false }

      it { is_expected.to be_nil }
    end

    context 'when kubernetes responds with valid pods' do
      it { is_expected.to eq(pods: [kube_pod]) }
    end

    context 'when kubernetes responds with 500' do
      let(:pods_response) { { status: 500 } }

      it { expect { subject }.to raise_error(KubeException) }
    end

    context 'when kubernetes responds with 404' do
      let(:pods_response) { { status: 404 } }

      it { is_expected.to eq(pods: []) }
    end
  end
end
