diff --git a/lib/nationbuilder_api/client.rb b/lib/nationbuilder_api/client.rb index a37a606..5b9424b 100644 --- a/lib/nationbuilder_api/client.rb +++ b/lib/nationbuilder_api/client.rb @@ -16,6 +16,7 @@ class Client # @option options [Logger] :logger Logger instance # @option options [Symbol] :log_level Log level (:debug, :info, :warn, :error) # @option options [Integer] :timeout HTTP timeout in seconds + # @option options [Boolean] :ssl_verify Enable SSL certificate verification (default: true) # @option options [String] :identifier Token identifier for multi-tenant apps def initialize(**options) @config = build_configuration(options) diff --git a/lib/nationbuilder_api/configuration.rb b/lib/nationbuilder_api/configuration.rb index 76da91e..ae5ad3e 100644 --- a/lib/nationbuilder_api/configuration.rb +++ b/lib/nationbuilder_api/configuration.rb @@ -6,17 +6,19 @@ module NationbuilderApi class Configuration attr_accessor :client_id, :client_secret, :redirect_uri, :base_url, - :log_level, :timeout + :log_level, :timeout, :ssl_verify attr_writer :logger, :token_adapter DEFAULT_BASE_URL = "https://api.nationbuilder.com/v2" DEFAULT_TIMEOUT = 30 DEFAULT_LOG_LEVEL = :info + DEFAULT_SSL_VERIFY = true def initialize @base_url = DEFAULT_BASE_URL @timeout = DEFAULT_TIMEOUT @log_level = DEFAULT_LOG_LEVEL + @ssl_verify = DEFAULT_SSL_VERIFY @logger = nil @token_adapter = nil @client_id = nil diff --git a/lib/nationbuilder_api/http_client.rb b/lib/nationbuilder_api/http_client.rb index 21c5251..eaea5c1 100644 --- a/lib/nationbuilder_api/http_client.rb +++ b/lib/nationbuilder_api/http_client.rb @@ -105,8 +105,8 @@ def execute_request(method, url, headers:, params:, body:) http.use_ssl = true http.read_timeout = config.timeout http.open_timeout = config.timeout - # SSL verification is always enabled for security - # Use OpenSSL::SSL::VERIFY_PEER by default (Ruby's default) + # SSL verification (can be disabled for development/testing) + http.verify_mode = config.ssl_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE # Create request object based on method request = case method diff --git a/lib/nationbuilder_api/resources/people.rb b/lib/nationbuilder_api/resources/people.rb index e3be674..6fce4a4 100644 --- a/lib/nationbuilder_api/resources/people.rb +++ b/lib/nationbuilder_api/resources/people.rb @@ -15,7 +15,7 @@ class People < Base # # @example # client.people.show(123) - # # => { data: { type: "signup", id: "123", attributes: { ... } } } + # # => { data: { type: "signups", id: "123", attributes: { ... } } } # # @example With taggings sideloaded # client.people.show(123, include_taggings: true) @@ -23,7 +23,7 @@ class People < Base # # @example Current user # client.people.show("me") - # # => { data: { type: "signup", id: "123", attributes: { ... } } } + # # => { data: { type: "signups", id: "123", attributes: { ... } } } def show(id, include_taggings: false) path = "/api/v2/signups/#{id}" path += "?include=taggings" if include_taggings @@ -117,7 +117,7 @@ def update(id, attributes:) path = "/api/v2/signups/#{id}" body = { data: { - type: "signup", + type: "signups", id: id.to_s, attributes: attributes } diff --git a/spec/nationbuilder_api/http_client_net_http_spec.rb b/spec/nationbuilder_api/http_client_net_http_spec.rb index 3b05275..c83d9fb 100644 --- a/spec/nationbuilder_api/http_client_net_http_spec.rb +++ b/spec/nationbuilder_api/http_client_net_http_spec.rb @@ -119,10 +119,53 @@ end describe "SSL verification configuration" do - it "always uses SSL verification regardless of Rails environment" do - # Mock Rails environment as development - rails_double = double("Rails", env: double("env", development?: true, test?: false)) - stub_const("Rails", rails_double) + it "uses VERIFY_PEER when ssl_verify is true (default)" do + # Mock Net::HTTP to verify SSL configuration + http_instance = instance_double(Net::HTTP) + allow(Net::HTTP).to receive(:new).and_return(http_instance) + allow(http_instance).to receive(:use_ssl=) + allow(http_instance).to receive(:read_timeout=) + allow(http_instance).to receive(:open_timeout=) + allow(http_instance).to receive(:verify_mode=) + + # Create a mock request and response + request_double = instance_double(Net::HTTP::Get) + allow(Net::HTTP::Get).to receive(:new).and_return(request_double) + allow(request_double).to receive(:[]=) + + response_double = instance_double(Net::HTTPSuccess, code: "200", body: '{"data": []}', to_hash: {"content-type" => ["application/json"]}) + allow(http_instance).to receive(:request).and_return(response_double) + + # Execute request with default ssl_verify: true + http_client.get("/people") + + # Verify SSL verification is enabled (VERIFY_PEER) + expect(http_instance).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER) + end + + it "uses VERIFY_NONE when ssl_verify is false" do + # Create client with ssl_verify disabled + config_with_ssl_disabled = NationbuilderApi::Configuration.new + config_with_ssl_disabled.base_url = "https://api.nationbuilder.com/v2" + config_with_ssl_disabled.client_id = "test_client_id" + config_with_ssl_disabled.client_secret = "test_client_secret" + config_with_ssl_disabled.redirect_uri = "https://example.com/callback" + config_with_ssl_disabled.ssl_verify = false + + token_adapter = NationbuilderApi::TokenStorage::Memory.new + token_adapter.store_token("test", { + access_token: "test_token", + refresh_token: "refresh_token", + expires_at: Time.now + 3600, + token_type: "Bearer", + scopes: [] + }) + + http_client_no_ssl = described_class.new( + config: config_with_ssl_disabled, + token_adapter: token_adapter, + identifier: "test" + ) # Mock Net::HTTP to verify SSL configuration http_instance = instance_double(Net::HTTP) @@ -141,11 +184,10 @@ allow(http_instance).to receive(:request).and_return(response_double) # Execute request - http_client.get("/people") + http_client_no_ssl.get("/people") - # Verify SSL verification was NOT explicitly disabled - # Ruby's Net::HTTP defaults to VERIFY_PEER, so we don't set verify_mode at all - expect(http_instance).not_to have_received(:verify_mode=) + # Verify SSL verification is disabled (VERIFY_NONE) + expect(http_instance).to have_received(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE) end it "enables SSL for HTTPS requests" do @@ -155,6 +197,7 @@ allow(http_instance).to receive(:use_ssl=) allow(http_instance).to receive(:read_timeout=) allow(http_instance).to receive(:open_timeout=) + allow(http_instance).to receive(:verify_mode=) # Create a mock request and response request_double = instance_double(Net::HTTP::Get) diff --git a/spec/nationbuilder_api/resources/people_spec.rb b/spec/nationbuilder_api/resources/people_spec.rb index 8f5f01c..d26fb7d 100644 --- a/spec/nationbuilder_api/resources/people_spec.rb +++ b/spec/nationbuilder_api/resources/people_spec.rb @@ -201,7 +201,7 @@ let(:expected_body) do { data: { - type: "signup", + type: "signups", id: "123", attributes: update_attributes } @@ -239,7 +239,7 @@ it "accepts string ID" do expected_body_with_string_id = { data: { - type: "signup", + type: "signups", id: "456", attributes: update_attributes } @@ -264,7 +264,7 @@ expected_address_body = { data: { - type: "signup", + type: "signups", id: "123", attributes: address_attributes } @@ -279,7 +279,7 @@ it "handles empty attributes hash" do empty_body = { data: { - type: "signup", + type: "signups", id: "123", attributes: {} } @@ -291,6 +291,14 @@ people.update(123, attributes: {}) end + it "sets type field to 'signups' in request body" do + expect(client).to receive(:patch) do |_path, options| + expect(options[:body][:data][:type]).to eq("signups") + end + + people.update(123, attributes: update_attributes) + end + it "raises NotFoundError when person does not exist" do allow(client).to receive(:patch) .and_raise(NationbuilderApi::NotFoundError, "Person not found")