Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/nationbuilder_api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion lib/nationbuilder_api/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/nationbuilder_api/http_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions lib/nationbuilder_api/resources/people.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ 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)
# # => { data: { ... }, included: [{ type: "tagging", ... }] }
#
# @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
Expand Down Expand Up @@ -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
}
Expand Down
59 changes: 51 additions & 8 deletions spec/nationbuilder_api/http_client_net_http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
16 changes: 12 additions & 4 deletions spec/nationbuilder_api/resources/people_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@
let(:expected_body) do
{
data: {
type: "signup",
type: "signups",
id: "123",
attributes: update_attributes
}
Expand Down Expand Up @@ -239,7 +239,7 @@
it "accepts string ID" do
expected_body_with_string_id = {
data: {
type: "signup",
type: "signups",
id: "456",
attributes: update_attributes
}
Expand All @@ -264,7 +264,7 @@

expected_address_body = {
data: {
type: "signup",
type: "signups",
id: "123",
attributes: address_attributes
}
Expand All @@ -279,7 +279,7 @@
it "handles empty attributes hash" do
empty_body = {
data: {
type: "signup",
type: "signups",
id: "123",
attributes: {}
}
Expand All @@ -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")
Expand Down