# Copyright 2020 Google LLC
#
# 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 "helper"

require "openssl"

describe Google::Auth::IDTokens do
  describe "StaticKeySource" do
    let(:key1) { Google::Auth::IDTokens::KeyInfo.new id: "1234", key: :key1, algorithm: "RS256" }
    let(:key2) { Google::Auth::IDTokens::KeyInfo.new id: "5678", key: :key2, algorithm: "ES256" }
    let(:keys) { [key1, key2] }
    let(:source) { Google::Auth::IDTokens::StaticKeySource.new keys }

    it "returns a static set of keys" do
      assert_equal keys, source.current_keys
    end

    it "does not change on refresh" do
      assert_equal keys, source.refresh_keys
    end
  end

  describe "HttpKeySource" do
    let(:certs_uri) { "https://example.com/my-certs" }
    let(:certs_body) { "{}" }

    it "raises an error when failing to parse json from the site" do
      source = Google::Auth::IDTokens::HttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: "whoops")
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to parse JSON", error.message
      assert_requested stub
    end

    it "downloads data but gets no keys" do
      source = Google::Auth::IDTokens::HttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: certs_body)
      keys = source.refresh_keys
      assert_empty keys
      assert_requested stub
    end
  end

  describe "X509CertHttpKeySource" do
    let(:certs_uri) { "https://example.com/my-certs" }
    let(:key1) { OpenSSL::PKey::RSA.new 2048 }
    let(:key2) { OpenSSL::PKey::RSA.new 2048 }
    let(:cert1) { generate_cert key1 }
    let(:cert2) { generate_cert key2 }
    let(:id1) { "1234" }
    let(:id2) { "5678" }
    let(:certs_body) { JSON.dump({ id1 => cert1.to_pem, id2 => cert2.to_pem }) }

    after do
      WebMock.reset!
    end

    def generate_cert key
      cert = OpenSSL::X509::Certificate.new
      cert.subject = cert.issuer = OpenSSL::X509::Name.parse "/C=BE/O=Test/OU=Test/CN=Test"
      cert.not_before = Time.now
      cert.not_after = Time.now + 365 * 24 * 60 * 60
      cert.public_key = key.public_key
      cert.serial = 0x0
      cert.version = 2
      cert.sign key, OpenSSL::Digest::SHA1.new
      cert
    end

    it "raises an error when failing to reach the site" do
      source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: "whoops", status: 404)
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to retrieve data from #{certs_uri}", error.message
      assert_requested stub
    end

    it "raises an error when failing to parse json from the site" do
      source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: "whoops")
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to parse JSON", error.message
      assert_requested stub
    end

    it "raises an error when failing to parse x509 from the site" do
      source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: '{"hi": "whoops"}')
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to parse X509 certificates", error.message
      assert_requested stub
    end

    it "gets the right certificates" do
      source = Google::Auth::IDTokens::X509CertHttpKeySource.new certs_uri
      stub = stub_request(:get, certs_uri).to_return(body: certs_body)
      keys = source.refresh_keys
      assert_equal id1, keys[0].id
      assert_equal id2, keys[1].id
      assert_equal key1.public_key.to_pem, keys[0].key.to_pem
      assert_equal key2.public_key.to_pem, keys[1].key.to_pem
      assert_equal "RS256", keys[0].algorithm
      assert_equal "RS256", keys[1].algorithm
      assert_requested stub
    end
  end

  describe "JwkHttpKeySource" do
    let(:jwk_uri) { "https://example.com/my-jwk" }
    let(:id1) { "fb8ca5b7d8d9a5c6c6788071e866c6c40f3fc1f9" }
    let(:id2) { "LYyP2g" }
    let(:jwk1) {
      {
        alg: "RS256",
        e:   "AQAB",
        kid: id1,
        kty: "RSA",
        n:   "zK8PHf_6V3G5rU-viUOL1HvAYn7q--dxMoUkt7x1rSWX6fimla-lpoYAKhFTLU" \
             "ELkRKy_6UDzfybz0P9eItqS2UxVWYpKYmKTQ08HgUBUde4GtO_B0SkSk8iLtGh" \
             "653UBBjgXmfzdfQEz_DsaWn7BMtuAhY9hpMtJye8LQlwaS8ibQrsC0j0GZM5KX" \
             "RITHwfx06_T1qqC_MOZRA6iJs-J2HNlgeyFuoQVBTY6pRqGXa-qaVsSG3iU-vq" \
             "NIciFquIq-xydwxLqZNksRRer5VAsSHf0eD3g2DX-cf6paSy1aM40svO9EfSvG" \
             "_07MuHafEE44RFvSZZ4ubEN9U7ALSjdw",
        use: "sig"
      }
    }
    let(:jwk2) {
      {
        alg: "ES256",
        crv: "P-256",
        kid: id2,
        kty: "EC",
        use: "sig",
        x:   "SlXFFkJ3JxMsXyXNrqzE3ozl_0913PmNbccLLWfeQFU",
        y:   "GLSahrZfBErmMUcHP0MGaeVnJdBwquhrhQ8eP05NfCI"
      }
    }
    let(:bad_type_jwk) {
      {
        alg: "RS256",
        kid: "hello",
        kty: "blah",
        use: "sig"
      }
    }
    let(:jwk_body) { JSON.dump({ keys: [jwk1, jwk2] }) }
    let(:bad_type_body) { JSON.dump({ keys: [bad_type_jwk] }) }

    def b64_to_hex b64
      Base64.urlsafe_decode64(b64).unpack1("H*").upcase
    end

    after do
      WebMock.reset!
    end

    it "raises an error when failing to reach the site" do
      source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
      stub = stub_request(:get, jwk_uri).to_return(body: "whoops", status: 404)
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to retrieve data from #{jwk_uri}", error.message
      assert_requested stub
    end

    it "raises an error when failing to parse json from the site" do
      source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
      stub = stub_request(:get, jwk_uri).to_return(body: "whoops")
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Unable to parse JSON", error.message
      assert_requested stub
    end

    it "raises an error when the json structure is malformed" do
      source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
      stub = stub_request(:get, jwk_uri).to_return(body: '{"hi": "whoops"}')
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "No keys found in jwk set", error.message
      assert_requested stub
    end

    it "raises an error when an unrecognized key type is encountered" do
      source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
      stub = stub_request(:get, jwk_uri).to_return(body: bad_type_body)
      error = assert_raises Google::Auth::IDTokens::KeySourceError do
        source.refresh_keys
      end
      assert_equal "Cannot use key type blah", error.message
      assert_requested stub
    end

    it "gets the right keys" do
      source = Google::Auth::IDTokens::JwkHttpKeySource.new jwk_uri
      stub = stub_request(:get, jwk_uri).to_return(body: jwk_body)
      keys = source.refresh_keys
      assert_equal id1, keys[0].id
      assert_equal id2, keys[1].id
      assert_equal "RS256", keys[0].algorithm
      assert_kind_of OpenSSL::PKey::RSA, keys[0].key
      assert_equal b64_to_hex(jwk1[:n]), keys[0].key.n.to_s(16)
      assert_equal b64_to_hex(jwk1[:e]), keys[0].key.e.to_s(16)
      assert_equal "ES256", keys[1].algorithm
      assert_kind_of OpenSSL::PKey::EC, keys[1].key
      assert_equal "prime256v1", keys[1].key.group.curve_name
      assert_equal "04#{b64_to_hex(jwk2[:x])}#{b64_to_hex(jwk2[:y])}",
                   keys[1].key.public_key.to_octet_string(:uncompressed).unpack1("H*").upcase
      assert_requested stub
    end
  end
end
