require File.expand_path("../../test_helper", __FILE__)

module Stripe
  class StripeClientTest < Test::Unit::TestCase
    context ".active_client" do
      should "be .default_client outside of #request" do
        assert_equal StripeClient.default_client, StripeClient.active_client
      end

      should "be active client inside of #request" do
        client = StripeClient.new
        client.request do
          assert_equal client, StripeClient.active_client
        end
      end
    end

    context ".default_client" do
      should "be a StripeClient" do
        assert_kind_of StripeClient, StripeClient.default_client
      end

      should "be a different client on each thread" do
        other_thread_client = nil
        thread = Thread.new do
          other_thread_client = StripeClient.default_client
        end
        thread.join
        refute_equal StripeClient.default_client, other_thread_client
      end
    end

    context ".default_conn" do
      should "be a Faraday::Connection" do
        assert_kind_of Faraday::Connection, StripeClient.default_conn
      end

      should "be a different connection on each thread" do
        other_thread_conn = nil
        thread = Thread.new do
          other_thread_conn = StripeClient.default_conn
        end
        thread.join
        refute_equal StripeClient.default_conn, other_thread_conn
      end
    end

    context ".should_retry?" do
      setup do
        Stripe.stubs(:max_network_retries).returns(2)
      end

      should "retry on timeout" do
        assert StripeClient.should_retry?(Faraday::TimeoutError.new(""), 0)
      end

      should "retry on a failed connection" do
        assert StripeClient.should_retry?(Faraday::ConnectionFailed.new(""), 0)
      end

      should "retry on a conflict" do
        error = make_rate_limit_error
        e = Faraday::ClientError.new(error[:error][:message], status: 409)
        assert StripeClient.should_retry?(e, 0)
      end

      should "not retry at maximum count" do
        refute StripeClient.should_retry?(RuntimeError.new, Stripe.max_network_retries)
      end

      should "not retry on a certificate validation error" do
        refute StripeClient.should_retry?(Faraday::SSLError.new(""), 0)
      end
    end

    context ".sleep_time" do
      should "should grow exponentially" do
        StripeClient.stubs(:rand).returns(1)
        Stripe.stubs(:max_network_retry_delay).returns(999)
        assert_equal(Stripe.initial_network_retry_delay, StripeClient.sleep_time(1))
        assert_equal(Stripe.initial_network_retry_delay * 2, StripeClient.sleep_time(2))
        assert_equal(Stripe.initial_network_retry_delay * 4, StripeClient.sleep_time(3))
        assert_equal(Stripe.initial_network_retry_delay * 8, StripeClient.sleep_time(4))
      end

      should "enforce the max_network_retry_delay" do
        StripeClient.stubs(:rand).returns(1)
        Stripe.stubs(:initial_network_retry_delay).returns(1)
        Stripe.stubs(:max_network_retry_delay).returns(2)
        assert_equal(1, StripeClient.sleep_time(1))
        assert_equal(2, StripeClient.sleep_time(2))
        assert_equal(2, StripeClient.sleep_time(3))
        assert_equal(2, StripeClient.sleep_time(4))
      end

      should "add some randomness" do
        random_value = 0.8
        StripeClient.stubs(:rand).returns(random_value)
        Stripe.stubs(:initial_network_retry_delay).returns(1)
        Stripe.stubs(:max_network_retry_delay).returns(8)

        base_value = Stripe.initial_network_retry_delay * (0.5 * (1 + random_value))

        # the initial value cannot be smaller than the base,
        # so the randomness is ignored
        assert_equal(Stripe.initial_network_retry_delay, StripeClient.sleep_time(1))

        # after the first one, the randomness is applied
        assert_equal(base_value * 2, StripeClient.sleep_time(2))
        assert_equal(base_value * 4, StripeClient.sleep_time(3))
        assert_equal(base_value * 8, StripeClient.sleep_time(4))
      end
    end

    context "#initialize" do
      should "set Stripe.default_conn" do
        client = StripeClient.new
        assert_equal StripeClient.default_conn, client.conn
      end

      should "set a different connection if one was specified" do
        conn = Faraday.new
        client = StripeClient.new(conn)
        assert_equal conn, client.conn
      end
    end

    context "#execute_request" do
      context "headers" do
        should "support literal headers" do
          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .with(headers: { "Stripe-Account" => "bar" })
            .to_return(body: JSON.generate(object: "account"))

          client = StripeClient.new
          client.execute_request(:post, "/v1/account",
                                 headers: { "Stripe-Account" => "bar" })
        end

        should "support RestClient-style header keys" do
          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .with(headers: { "Stripe-Account" => "bar" })
            .to_return(body: JSON.generate(object: "account"))

          client = StripeClient.new
          client.execute_request(:post, "/v1/account",
                                 headers: { stripe_account: "bar" })
        end
      end

      context "logging" do
        setup do
          # Freeze time for the purposes of the `elapsed` parameter that we
          # emit for responses. I didn't want to bring in a new dependency for
          # this, but Mocha's `anything` parameter can't match inside of a hash
          # and is therefore not useful for this purpose. If we switch over to
          # rspec-mocks at some point, we can probably remove Timecop from the
          # project.
          Timecop.freeze(Time.local(1990))
        end

        teardown do
          Timecop.return
        end

        should "produce appropriate logging" do
          body = JSON.generate(object: "account")

          Util.expects(:log_info).with("Request to Stripe API",
                                       account: "acct_123",
                                       api_version: "2010-11-12",
                                       idempotency_key: "abc",
                                       method: :post,
                                       num_retries: 0,
                                       path: "/v1/account")
          Util.expects(:log_debug).with("Request details",
                                        body: "",
                                        idempotency_key: "abc",
                                        query_string: nil)

          Util.expects(:log_info).with("Response from Stripe API",
                                       account: "acct_123",
                                       api_version: "2010-11-12",
                                       elapsed: 0.0,
                                       idempotency_key: "abc",
                                       method: :post,
                                       path: "/v1/account",
                                       request_id: "req_123",
                                       status: 200)
          Util.expects(:log_debug).with("Response details",
                                        body: body,
                                        idempotency_key: "abc",
                                        request_id: "req_123")
          Util.expects(:log_debug).with("Dashboard link for request",
                                        idempotency_key: "abc",
                                        request_id: "req_123",
                                        url: Util.request_id_dashboard_url("req_123", Stripe.api_key))

          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .to_return(
              body: body,
              headers: {
                "Idempotency-Key" => "abc",
                "Request-Id" => "req_123",
                "Stripe-Account" => "acct_123",
                "Stripe-Version" => "2010-11-12",
              }
            )

          client = StripeClient.new
          client.execute_request(:post, "/v1/account",
                                 headers: {
                                   "Idempotency-Key" => "abc",
                                   "Stripe-Account" => "acct_123",
                                   "Stripe-Version" => "2010-11-12",
                                 })
        end

        should "produce logging on API error" do
          Util.expects(:log_info).with("Request to Stripe API",
                                       account: nil,
                                       api_version: nil,
                                       idempotency_key: nil,
                                       method: :post,
                                       num_retries: 0,
                                       path: "/v1/account")
          Util.expects(:log_info).with("Response from Stripe API",
                                       account: nil,
                                       api_version: nil,
                                       elapsed: 0.0,
                                       idempotency_key: nil,
                                       method: :post,
                                       path: "/v1/account",
                                       request_id: nil,
                                       status: 500)

          error = {
            code: "code",
            message: "message",
            param: "param",
            type: "type",
          }
          Util.expects(:log_error).with("Stripe API error",
                                        status: 500,
                                        error_code: error["code"],
                                        error_message: error["message"],
                                        error_param: error["param"],
                                        error_type: error["type"],
                                        idempotency_key: nil,
                                        request_id: nil)

          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .to_return(
              body: JSON.generate(error: error),
              status: 500
            )

          client = StripeClient.new
          assert_raises Stripe::APIError do
            client.execute_request(:post, "/v1/account")
          end
        end

        should "produce logging on OAuth error" do
          Util.expects(:log_info).with("Request to Stripe API",
                                       account: nil,
                                       api_version: nil,
                                       idempotency_key: nil,
                                       method: :post,
                                       num_retries: 0,
                                       path: "/oauth/token")
          Util.expects(:log_info).with("Response from Stripe API",
                                       account: nil,
                                       api_version: nil,
                                       elapsed: 0.0,
                                       idempotency_key: nil,
                                       method: :post,
                                       path: "/oauth/token",
                                       request_id: nil,
                                       status: 400)

          Util.expects(:log_error).with("Stripe OAuth error",
                                        status: 400,
                                        error_code: "invalid_request",
                                        error_description: "No grant type specified",
                                        idempotency_key: nil,
                                        request_id: nil)

          stub_request(:post, "#{Stripe.connect_base}/oauth/token")
            .to_return(body: JSON.generate(error: "invalid_request",
                                           error_description: "No grant type specified"), status: 400)

          client = StripeClient.new
          opts = { api_base: Stripe.connect_base }
          assert_raises Stripe::OAuth::InvalidRequestError do
            client.execute_request(:post, "/oauth/token", opts)
          end
        end
      end

      context "Stripe-Account header" do
        should "use a globally set header" do
          begin
            old = Stripe.stripe_account
            Stripe.stripe_account = "acct_1234"

            stub_request(:post, "#{Stripe.api_base}/v1/account")
              .with(headers: { "Stripe-Account" => Stripe.stripe_account })
              .to_return(body: JSON.generate(object: "account"))

            client = StripeClient.new
            client.execute_request(:post, "/v1/account")
          ensure
            Stripe.stripe_account = old
          end
        end

        should "use a locally set header" do
          stripe_account = "acct_0000"
          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .with(headers: { "Stripe-Account" => stripe_account })
            .to_return(body: JSON.generate(object: "account"))

          client = StripeClient.new
          client.execute_request(:post, "/v1/account",
                                 headers: { stripe_account: stripe_account })
        end

        should "not send it otherwise" do
          stub_request(:post, "#{Stripe.api_base}/v1/account")
            .with do |req|
              req.headers["Stripe-Account"].nil?
            end.to_return(body: JSON.generate(object: "account"))

          client = StripeClient.new
          client.execute_request(:post, "/v1/account")
        end
      end

      context "app_info" do
        should "send app_info if set" do
          begin
            old = Stripe.app_info
            Stripe.set_app_info(
              "MyAwesomePlugin",
              url: "https://myawesomeplugin.info",
              version: "1.2.34"
            )

            stub_request(:post, "#{Stripe.api_base}/v1/account")
              .with do |req|
                assert_equal \
                  "Stripe/v1 RubyBindings/#{Stripe::VERSION} " \
                  "MyAwesomePlugin/1.2.34 (https://myawesomeplugin.info)",
                  req.headers["User-Agent"]

                data = JSON.parse(req.headers["X-Stripe-Client-User-Agent"],
                                  symbolize_names: true)

                assert_equal({
                  name: "MyAwesomePlugin",
                  url: "https://myawesomeplugin.info",
                  version: "1.2.34",
                }, data[:application])

                true
              end.to_return(body: JSON.generate(object: "account"))

            client = StripeClient.new
            client.execute_request(:post, "/v1/account")
          ensure
            Stripe.app_info = old
          end
        end
      end

      context "error handling" do
        should "handle error response with empty body" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: "", status: 500)

          client = StripeClient.new
          e = assert_raises Stripe::APIError do
            client.execute_request(:post, "/v1/charges")
          end

          assert_equal 'Invalid response object from API: "" (HTTP response code was 500)', e.message
        end

        should "handle success response with empty body" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: "", status: 200)

          client = StripeClient.new
          e = assert_raises Stripe::APIError do
            client.execute_request(:post, "/v1/charges")
          end

          assert_equal 'Invalid response object from API: "" (HTTP response code was 200)', e.message
        end

        should "handle error response with unknown value" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(bar: "foo"), status: 500)

          client = StripeClient.new
          e = assert_raises Stripe::APIError do
            client.execute_request(:post, "/v1/charges")
          end

          assert_equal 'Invalid response object from API: "{\"bar\":\"foo\"}" (HTTP response code was 500)', e.message
        end

        should "raise InvalidRequestError on 400" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_missing_id_error), status: 400)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::InvalidRequestError => e
            assert_equal(400, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise AuthenticationError on 401" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_missing_id_error), status: 401)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::AuthenticationError => e
            assert_equal(401, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise CardError on 402" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_missing_id_error), status: 402)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::CardError => e
            assert_equal(402, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise PermissionError on 403" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_missing_id_error), status: 403)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::PermissionError => e
            assert_equal(403, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise InvalidRequestError on 404" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_missing_id_error), status: 404)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::InvalidRequestError => e
            assert_equal(404, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise RateLimitError on 429" do
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return(body: JSON.generate(make_rate_limit_error), status: 429)
          client = StripeClient.new
          begin
            client.execute_request(:post, "/v1/charges")
          rescue Stripe::RateLimitError => e
            assert_equal(429, e.http_status)
            assert_equal(true, e.json_body.is_a?(Hash))
          end
        end

        should "raise OAuth::InvalidRequestError when error is a string with value 'invalid_request'" do
          stub_request(:post, "#{Stripe.connect_base}/oauth/token")
            .to_return(body: JSON.generate(error: "invalid_request",
                                           error_description: "No grant type specified"), status: 400)

          client = StripeClient.new
          opts = { api_base: Stripe.connect_base }
          e = assert_raises Stripe::OAuth::InvalidRequestError do
            client.execute_request(:post, "/oauth/token", opts)
          end

          assert_equal(400, e.http_status)
          assert_equal("No grant type specified", e.message)
        end

        should "raise OAuth::InvalidGrantError when error is a string with value 'invalid_grant'" do
          stub_request(:post, "#{Stripe.connect_base}/oauth/token")
            .to_return(body: JSON.generate(error: "invalid_grant",
                                           error_description: "This authorization code has already been used. All tokens issued with this code have been revoked."), status: 400)

          client = StripeClient.new
          opts = { api_base: Stripe.connect_base }
          e = assert_raises Stripe::OAuth::InvalidGrantError do
            client.execute_request(:post, "/oauth/token", opts)
          end

          assert_equal(400, e.http_status)
          assert_equal("invalid_grant", e.code)
          assert_equal("This authorization code has already been used. All tokens issued with this code have been revoked.", e.message)
        end

        should "raise OAuth::InvalidClientError when error is a string with value 'invalid_client'" do
          stub_request(:post, "#{Stripe.connect_base}/oauth/deauthorize")
            .to_return(body: JSON.generate(error: "invalid_client",
                                           error_description: "This application is not connected to stripe account acct_19tLK7DSlTMT26Mk, or that account does not exist."), status: 401)

          client = StripeClient.new
          opts = { api_base: Stripe.connect_base }
          e = assert_raises Stripe::OAuth::InvalidClientError do
            client.execute_request(:post, "/oauth/deauthorize", opts)
          end

          assert_equal(401, e.http_status)
          assert_equal("invalid_client", e.code)
          assert_equal("This application is not connected to stripe account acct_19tLK7DSlTMT26Mk, or that account does not exist.", e.message)
        end

        should "raise Stripe::OAuthError on indeterminate OAuth error" do
          stub_request(:post, "#{Stripe.connect_base}/oauth/deauthorize")
            .to_return(body: JSON.generate(error: "new_code_not_recognized",
                                           error_description: "Something."), status: 401)

          client = StripeClient.new
          opts = { api_base: Stripe.connect_base }
          e = assert_raises Stripe::OAuth::OAuthError do
            client.execute_request(:post, "/oauth/deauthorize", opts)
          end

          assert_equal(401, e.http_status)
          assert_equal("new_code_not_recognized", e.code)
          assert_equal("Something.", e.message)
        end
      end

      context "idempotency keys" do
        setup do
          Stripe.stubs(:max_network_retries).returns(2)
        end

        should "not add an idempotency key to GET requests" do
          SecureRandom.expects(:uuid).times(0)
          stub_request(:get, "#{Stripe.api_base}/v1/charges/ch_123")
            .with do |req|
              req.headers["Idempotency-Key"].nil?
            end.to_return(body: JSON.generate(object: "charge"))
          client = StripeClient.new
          client.execute_request(:get, "/v1/charges/ch_123")
        end

        should "ensure there is always an idempotency_key on POST requests" do
          SecureRandom.expects(:uuid).at_least_once.returns("random_key")
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .with(headers: { "Idempotency-Key" => "random_key" })
            .to_return(body: JSON.generate(object: "charge"))
          client = StripeClient.new
          client.execute_request(:post, "/v1/charges")
        end

        should "ensure there is always an idempotency_key on DELETE requests" do
          SecureRandom.expects(:uuid).at_least_once.returns("random_key")
          stub_request(:delete, "#{Stripe.api_base}/v1/charges/ch_123")
            .with(headers: { "Idempotency-Key" => "random_key" })
            .to_return(body: JSON.generate(object: "charge"))
          client = StripeClient.new
          client.execute_request(:delete, "/v1/charges/ch_123")
        end

        should "not override a provided idempotency_key" do
          # Note that this expectation looks like `:idempotency_key` instead of
          # the header `Idempotency-Key` because it's user provided as seen
          # below. The ones injected by the library itself look like headers
          # (`Idempotency-Key`), but rest-client does allow this symbol
          # formatting and will properly override the system generated one as
          # expected.
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .with(headers: { "Idempotency-Key" => "provided_key" })
            .to_return(body: JSON.generate(object: "charge"))

          client = StripeClient.new
          client.execute_request(:post, "/v1/charges",
                                 headers: { idempotency_key: "provided_key" })
        end
      end

      context "retry logic" do
        setup do
          Stripe.stubs(:max_network_retries).returns(2)
        end

        should "retry failed requests and raise if error persists" do
          StripeClient.expects(:sleep_time).at_least_once.returns(0)
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_raise(Errno::ECONNREFUSED.new)

          client = StripeClient.new
          err = assert_raises Stripe::APIConnectionError do
            client.execute_request(:post, "/v1/charges")
          end
          assert_match(/Request was retried 2 times/, err.message)
        end

        should "retry failed requests and return successful response" do
          StripeClient.expects(:sleep_time).at_least_once.returns(0)

          i = 0
          stub_request(:post, "#{Stripe.api_base}/v1/charges")
            .to_return do |_|
              if i < 2
                i += 1
                raise Errno::ECONNREFUSED
              else
                { body: JSON.generate("id" => "myid") }
              end
            end

          client = StripeClient.new
          client.execute_request(:post, "/v1/charges")
        end
      end

      context "params serialization" do
        should "allows empty strings in params" do
          client = StripeClient.new
          client.execute_request(:get, "/v1/invoices/upcoming", params: {
            customer: "cus_123",
            coupon: "",
          })
          assert_requested(
            :get,
            "#{Stripe.api_base}/v1/invoices/upcoming?",
            query: {
              customer: "cus_123",
              coupon: "",
            }
          )
        end

        should "filter nils in params" do
          client = StripeClient.new
          client.execute_request(:get, "/v1/invoices/upcoming", params: {
            customer: "cus_123",
            coupon: nil,
          })
          assert_requested(
            :get,
            "#{Stripe.api_base}/v1/invoices/upcoming?",
            query: {
              customer: "cus_123",
            }
          )
        end
      end
    end

    context "#request" do
      should "return a result and response object" do
        stub_request(:post, "#{Stripe.api_base}/v1/charges")
          .to_return(body: JSON.generate(object: "charge"))

        client = StripeClient.new
        charge, resp = client.request { Charge.create }

        assert charge.is_a?(Charge)
        assert resp.is_a?(StripeResponse)
        assert_equal 200, resp.http_status
      end

      should "return the value of given block" do
        client = StripeClient.new
        ret, = client.request { 7 }
        assert_equal 7, ret
      end

      should "reset local thread state after a call" do
        begin
          Thread.current[:stripe_client] = :stripe_client

          client = StripeClient.new
          client.request {}

          assert_equal :stripe_client, Thread.current[:stripe_client]
        ensure
          Thread.current[:stripe_client] = nil
        end
      end
    end
  end

  class SystemProfilerTest < Test::Unit::TestCase
    context "#uname" do
      should "run without failure" do
        # Don't actually check the result because we try a variety of different
        # strategies that will have different results depending on where this
        # test and running. We're mostly making sure that no exception is thrown.
        _ = StripeClient::SystemProfiler.uname
      end
    end

    context "#uname_from_system" do
      should "run without failure" do
        # as above, just verify that an exception is not thrown
        _ = StripeClient::SystemProfiler.uname_from_system
      end
    end

    context "#uname_from_system_ver" do
      should "run without failure" do
        # as above, just verify that an exception is not thrown
        _ = StripeClient::SystemProfiler.uname_from_system_ver
      end
    end
  end
end