mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
feat: add SSL_CA_FILE and SSL_VERIFY environment variables to support… (#894)
* feat: add SSL_CA_FILE and SSL_VERIFY environment variables to support self-signed certificates in self-hosted environments * fix: NoMethodError by defining SSL helper methods before configure block executes * refactor: Refactor SessionsController to use shared SslConfigurable module and simplify SSL initializer redundant checks * refactor: improve SSL configuration robustness and error detection accuracy * fix:HTTParty SSL options, add file validation guards, prevent Tempfile GC, and redact URLs in error logs * fix: Fix SSL concern indentation and stub Simplefin POST correctly in tests * fix: normalize ssl_verify to always return boolean instead of nil * fix: solve failing SimpleFin test * refactor: trim unused error-handling code from SslConfigurable, replace Tempfile with fixed-path CA bundle, fix namespace pollution in initializers, and add unit tests for core SSL configuration and Langfuse CRL callback. * fix: added require ileutils in the initializer and require ostruct in the test file. * fix: solve autoload conflict that broke provider loading, validate all certs in PEM bundles, and add missing requires.
This commit is contained in:
165
test/models/concerns/ssl_configurable_test.rb
Normal file
165
test/models/concerns/ssl_configurable_test.rb
Normal file
@@ -0,0 +1,165 @@
|
||||
require "test_helper"
|
||||
|
||||
class SslConfigurableTest < ActiveSupport::TestCase
|
||||
# Create a simple test host that extends SslConfigurable, mirroring how
|
||||
# providers use it in the actual codebase.
|
||||
class SslTestHost
|
||||
extend SslConfigurable
|
||||
end
|
||||
|
||||
setup do
|
||||
# Snapshot original config so we can restore it in teardown
|
||||
@original_verify = Rails.configuration.x.ssl.verify
|
||||
@original_ca_file = Rails.configuration.x.ssl.ca_file
|
||||
@original_debug = Rails.configuration.x.ssl.debug
|
||||
end
|
||||
|
||||
teardown do
|
||||
Rails.configuration.x.ssl.verify = @original_verify
|
||||
Rails.configuration.x.ssl.ca_file = @original_ca_file
|
||||
Rails.configuration.x.ssl.debug = @original_debug
|
||||
end
|
||||
|
||||
# -- ssl_verify? --
|
||||
|
||||
test "ssl_verify? returns true when verify is nil (default)" do
|
||||
Rails.configuration.x.ssl.verify = nil
|
||||
assert SslTestHost.ssl_verify?
|
||||
end
|
||||
|
||||
test "ssl_verify? returns true when verify is true" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
assert SslTestHost.ssl_verify?
|
||||
end
|
||||
|
||||
test "ssl_verify? returns false when verify is explicitly false" do
|
||||
Rails.configuration.x.ssl.verify = false
|
||||
refute SslTestHost.ssl_verify?
|
||||
end
|
||||
|
||||
# -- ssl_ca_file --
|
||||
|
||||
test "ssl_ca_file returns nil when no CA file is configured" do
|
||||
Rails.configuration.x.ssl.ca_file = nil
|
||||
assert_nil SslTestHost.ssl_ca_file
|
||||
end
|
||||
|
||||
test "ssl_ca_file returns the configured path" do
|
||||
Rails.configuration.x.ssl.ca_file = "/certs/my-ca.crt"
|
||||
assert_equal "/certs/my-ca.crt", SslTestHost.ssl_ca_file
|
||||
end
|
||||
|
||||
# -- ssl_debug? --
|
||||
|
||||
test "ssl_debug? returns false when debug is nil" do
|
||||
Rails.configuration.x.ssl.debug = nil
|
||||
refute SslTestHost.ssl_debug?
|
||||
end
|
||||
|
||||
test "ssl_debug? returns true when debug is true" do
|
||||
Rails.configuration.x.ssl.debug = true
|
||||
assert SslTestHost.ssl_debug?
|
||||
end
|
||||
|
||||
# -- faraday_ssl_options --
|
||||
|
||||
test "faraday_ssl_options returns verify true with no CA file by default" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
Rails.configuration.x.ssl.ca_file = nil
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.faraday_ssl_options
|
||||
|
||||
assert_equal true, options[:verify]
|
||||
assert_nil options[:ca_file]
|
||||
end
|
||||
|
||||
test "faraday_ssl_options includes ca_file when configured" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
Rails.configuration.x.ssl.ca_file = "/certs/my-ca.crt"
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.faraday_ssl_options
|
||||
|
||||
assert_equal true, options[:verify]
|
||||
assert_equal "/certs/my-ca.crt", options[:ca_file]
|
||||
end
|
||||
|
||||
test "faraday_ssl_options returns verify false when verification disabled" do
|
||||
Rails.configuration.x.ssl.verify = false
|
||||
Rails.configuration.x.ssl.ca_file = nil
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.faraday_ssl_options
|
||||
|
||||
assert_equal false, options[:verify]
|
||||
end
|
||||
|
||||
test "faraday_ssl_options includes both verify false and ca_file when both configured" do
|
||||
Rails.configuration.x.ssl.verify = false
|
||||
Rails.configuration.x.ssl.ca_file = "/certs/my-ca.crt"
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.faraday_ssl_options
|
||||
|
||||
assert_equal false, options[:verify]
|
||||
assert_equal "/certs/my-ca.crt", options[:ca_file]
|
||||
end
|
||||
|
||||
# -- httparty_ssl_options --
|
||||
|
||||
test "httparty_ssl_options returns verify true with no CA file by default" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
Rails.configuration.x.ssl.ca_file = nil
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.httparty_ssl_options
|
||||
|
||||
assert_equal true, options[:verify]
|
||||
assert_nil options[:ssl_ca_file]
|
||||
end
|
||||
|
||||
test "httparty_ssl_options includes ssl_ca_file when configured" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
Rails.configuration.x.ssl.ca_file = "/certs/my-ca.crt"
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.httparty_ssl_options
|
||||
|
||||
assert_equal true, options[:verify]
|
||||
assert_equal "/certs/my-ca.crt", options[:ssl_ca_file]
|
||||
end
|
||||
|
||||
test "httparty_ssl_options returns verify false when verification disabled" do
|
||||
Rails.configuration.x.ssl.verify = false
|
||||
Rails.configuration.x.ssl.ca_file = nil
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
options = SslTestHost.httparty_ssl_options
|
||||
|
||||
assert_equal false, options[:verify]
|
||||
end
|
||||
|
||||
# -- net_http_verify_mode --
|
||||
|
||||
test "net_http_verify_mode returns VERIFY_PEER when verification enabled" do
|
||||
Rails.configuration.x.ssl.verify = true
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
assert_equal OpenSSL::SSL::VERIFY_PEER, SslTestHost.net_http_verify_mode
|
||||
end
|
||||
|
||||
test "net_http_verify_mode returns VERIFY_NONE when verification disabled" do
|
||||
Rails.configuration.x.ssl.verify = false
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
assert_equal OpenSSL::SSL::VERIFY_NONE, SslTestHost.net_http_verify_mode
|
||||
end
|
||||
|
||||
test "net_http_verify_mode returns VERIFY_PEER when verify is nil" do
|
||||
Rails.configuration.x.ssl.verify = nil
|
||||
Rails.configuration.x.ssl.debug = false
|
||||
|
||||
assert_equal OpenSSL::SSL::VERIFY_PEER, SslTestHost.net_http_verify_mode
|
||||
end
|
||||
end
|
||||
66
test/models/eval/langfuse_client_test.rb
Normal file
66
test/models/eval/langfuse_client_test.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
require "test_helper"
|
||||
require "ostruct"
|
||||
|
||||
class Eval::LangfuseClientTest < ActiveSupport::TestCase
|
||||
# -- CRL error list --
|
||||
|
||||
test "crl_errors includes standard CRL error codes" do
|
||||
errors = Eval::Langfuse::Client.crl_errors
|
||||
|
||||
assert_includes errors, OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL
|
||||
assert_includes errors, OpenSSL::X509::V_ERR_CRL_HAS_EXPIRED
|
||||
assert_includes errors, OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID
|
||||
end
|
||||
|
||||
test "crl_errors is frozen" do
|
||||
assert Eval::Langfuse::Client.crl_errors.frozen?
|
||||
end
|
||||
|
||||
# -- CRL verify callback behavior --
|
||||
# The callback should bypass only CRL-specific errors while preserving the
|
||||
# original verification result for all other error types.
|
||||
|
||||
test "CRL callback returns true for CRL-unavailable errors" do
|
||||
crl_error_codes = Eval::Langfuse::Client.crl_errors
|
||||
store_ctx = OpenStruct.new(error: OpenSSL::X509::V_ERR_UNABLE_TO_GET_CRL)
|
||||
|
||||
callback = build_crl_callback(crl_error_codes)
|
||||
|
||||
assert callback.call(false, store_ctx), "CRL errors should be bypassed even when preverify_ok is false"
|
||||
end
|
||||
|
||||
test "CRL callback preserves preverify_ok for non-CRL errors" do
|
||||
crl_error_codes = Eval::Langfuse::Client.crl_errors
|
||||
# V_OK (0) is not a CRL error
|
||||
store_ctx = OpenStruct.new(error: 0)
|
||||
|
||||
callback = build_crl_callback(crl_error_codes)
|
||||
|
||||
assert callback.call(true, store_ctx), "Non-CRL errors with preverify_ok=true should pass"
|
||||
refute callback.call(false, store_ctx), "Non-CRL errors with preverify_ok=false should fail"
|
||||
end
|
||||
|
||||
test "CRL callback rejects cert errors that are not CRL-related" do
|
||||
crl_error_codes = Eval::Langfuse::Client.crl_errors
|
||||
# V_ERR_CERT_HAS_EXPIRED is a real cert error, not CRL
|
||||
store_ctx = OpenStruct.new(error: OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED)
|
||||
|
||||
callback = build_crl_callback(crl_error_codes)
|
||||
|
||||
refute callback.call(false, store_ctx), "Non-CRL cert errors should not be bypassed"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Reconstructs the same lambda used in Eval::Langfuse::Client#execute_request
|
||||
# for isolated testing without needing a real Net::HTTP connection.
|
||||
def build_crl_callback(crl_error_codes)
|
||||
->(preverify_ok, store_ctx) {
|
||||
if crl_error_codes.include?(store_ctx.error)
|
||||
true
|
||||
else
|
||||
preverify_ok
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
# First call raises timeout, second call succeeds
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
@@ -25,7 +25,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "retries on Net::OpenTimeout and succeeds on retry" do
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(Net::OpenTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
@@ -39,7 +39,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "retries on SocketError and succeeds on retry" do
|
||||
mock_response = OpenStruct.new(code: 200, body: '{"accounts": []}')
|
||||
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(2)
|
||||
.raises(SocketError.new("Failed to open TCP connection"))
|
||||
.then.returns(mock_response)
|
||||
@@ -51,7 +51,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "raises SimplefinError after max retries exceeded" do
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(4) # Initial + 3 retries
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
|
||||
@@ -66,7 +66,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
end
|
||||
|
||||
test "does not retry on non-retryable errors" do
|
||||
HTTParty.expects(:get)
|
||||
Provider::Simplefin.expects(:get)
|
||||
.times(1)
|
||||
.raises(ArgumentError.new("Invalid argument"))
|
||||
|
||||
@@ -80,7 +80,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "handles HTTP 429 rate limit response" do
|
||||
mock_response = OpenStruct.new(code: 429, body: "Rate limit exceeded")
|
||||
|
||||
HTTParty.expects(:get).returns(mock_response)
|
||||
Provider::Simplefin.expects(:get).returns(mock_response)
|
||||
|
||||
error = assert_raises(Provider::Simplefin::SimplefinError) do
|
||||
@provider.get_accounts(@access_url)
|
||||
@@ -93,7 +93,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
test "handles HTTP 500 server error response" do
|
||||
mock_response = OpenStruct.new(code: 500, body: "Internal Server Error")
|
||||
|
||||
HTTParty.expects(:get).returns(mock_response)
|
||||
Provider::Simplefin.expects(:get).returns(mock_response)
|
||||
|
||||
error = assert_raises(Provider::Simplefin::SimplefinError) do
|
||||
@provider.get_accounts(@access_url)
|
||||
@@ -106,7 +106,7 @@ class Provider::SimplefinTest < ActiveSupport::TestCase
|
||||
setup_token = Base64.encode64("https://example.com/claim")
|
||||
mock_response = OpenStruct.new(code: 200, body: "https://example.com/access")
|
||||
|
||||
HTTParty.expects(:post)
|
||||
Provider::Simplefin.expects(:post)
|
||||
.times(2)
|
||||
.raises(Net::ReadTimeout.new("Connection timed out"))
|
||||
.then.returns(mock_response)
|
||||
|
||||
Reference in New Issue
Block a user