diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac92501..5cd51b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### Unreleased +* Fixed HTTParty content type issue when request body is nil - POST, PUT, PATCH, and DELETE now default to empty object to ensure Content-Type: application/json is sent (#536) +* Added support for request_body parameter on DELETE (e.g. cancellation_reason for bookings) (#536) + ### [6.7.1] * Fix large attachment handling with string keys and custom content_ids diff --git a/lib/nylas/handler/api_operations.rb b/lib/nylas/handler/api_operations.rb index b1d59560..873ba423 100644 --- a/lib/nylas/handler/api_operations.rb +++ b/lib/nylas/handler/api_operations.rb @@ -63,6 +63,7 @@ module Post # @param path [String] Destination path for the call. # @param query_params [Hash, {}] Query params to pass to the call. # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return [Array(Hash, String, Hash)] Nylas data object, API Request ID, and response headers. def post(path:, query_params: {}, request_body: nil, headers: {}) @@ -70,7 +71,7 @@ def post(path:, query_params: {}, request_body: nil, headers: {}) method: :post, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -90,6 +91,7 @@ module Put # @param path [String] Destination path for the call. # @param query_params [Hash, {}] Query params to pass to the call. # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def put(path:, query_params: {}, request_body: nil, headers: {}) @@ -97,7 +99,7 @@ def put(path:, query_params: {}, request_body: nil, headers: {}) method: :put, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -117,6 +119,7 @@ module Patch # @param path [String] Destination path for the call. # @param query_params [Hash, {}] Query params to pass to the call. # @param request_body [Hash, nil] Request body to pass to the call. + # Defaults to {} when nil to ensure Content-Type: application/json is sent. # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. def patch(path:, query_params: {}, request_body: nil, headers: {}) @@ -124,7 +127,7 @@ def patch(path:, query_params: {}, request_body: nil, headers: {}) method: :patch, path: path, query: query_params, - payload: request_body, + payload: request_body || {}, headers: headers, api_key: api_key, timeout: timeout @@ -143,15 +146,17 @@ module Delete # # @param path [String] Destination path for the call. # @param query_params [Hash, {}] Query params to pass to the call. + # @param request_body [Hash, nil] Optional request body (e.g. cancellation_reason for bookings). + # Defaults to {} to ensure Content-Type: application/json is sent. # @param headers [Hash, {}] Additional HTTP headers to include in the payload. # @return Nylas data object and API Request ID. - def delete(path:, query_params: {}, headers: {}) + def delete(path:, query_params: {}, request_body: nil, headers: {}) response = execute( method: :delete, path: path, query: query_params, headers: headers, - payload: nil, + payload: request_body || {}, api_key: api_key, timeout: timeout ) diff --git a/lib/nylas/resources/bookings.rb b/lib/nylas/resources/bookings.rb index e357fc73..62406381 100644 --- a/lib/nylas/resources/bookings.rb +++ b/lib/nylas/resources/bookings.rb @@ -64,11 +64,13 @@ def confirm(booking_id:, request_body:, query_params: nil) # Delete a booking. # @param booking_id [String] The id of the booking to delete. # @param query_params [Hash, nil] Query params to pass to the request. + # @param request_body [Hash, nil] Optional body params (e.g. cancellation_reason). # @return [Array(TrueClass, String)] True and the API Request ID for the delete operation. - def destroy(booking_id:, query_params: nil) + def destroy(booking_id:, query_params: nil, request_body: nil) _, request_id = delete( path: "#{api_uri}/v3/scheduling/bookings/#{booking_id}", - query_params: query_params + query_params: query_params, + request_body: request_body ) [true, request_id] diff --git a/spec/nylas/handler/api_operations_spec.rb b/spec/nylas/handler/api_operations_spec.rb index df6c5739..bb12dfe9 100644 --- a/spec/nylas/handler/api_operations_spec.rb +++ b/spec/nylas/handler/api_operations_spec.rb @@ -143,7 +143,7 @@ def initialize(api_key, api_uri, timeout) method: :post, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -164,7 +164,7 @@ def initialize(api_key, api_uri, timeout) method: :post, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -174,6 +174,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id], nil]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :post, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:post, path: path, request_body: request_body) + end end end @@ -206,7 +222,7 @@ def initialize(api_key, api_uri, timeout) method: :put, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -216,6 +232,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :put, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:put, path: path, request_body: request_body) + end end end @@ -248,7 +280,7 @@ def initialize(api_key, api_uri, timeout) method: :patch, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -258,6 +290,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { foo: "bar" } + allow(api_operations).to receive(:execute).with( + method: :patch, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:patch, path: path, request_body: request_body) + end end end @@ -271,7 +319,7 @@ def initialize(api_key, api_uri, timeout) method: :delete, path: path, query: query_params, - payload: nil, + payload: {}, headers: headers, api_key: api_key, timeout: timeout @@ -288,7 +336,7 @@ def initialize(api_key, api_uri, timeout) method: :delete, path: path, query: {}, - payload: nil, + payload: {}, headers: {}, api_key: api_key, timeout: timeout @@ -298,6 +346,22 @@ def initialize(api_key, api_uri, timeout) expect(response).to eq([mock_response[:data], mock_response[:request_id]]) end + + it "passes request_body to execute when provided" do + path = "#{api_uri}/path" + request_body = { cancellation_reason: "Meeting cancelled" } + allow(api_operations).to receive(:execute).with( + method: :delete, + path: path, + query: {}, + payload: request_body, + headers: {}, + api_key: api_key, + timeout: timeout + ).and_return(mock_response) + + api_operations.send(:delete, path: path, request_body: request_body) + end end end end diff --git a/spec/nylas/handler/http_client_integration_spec.rb b/spec/nylas/handler/http_client_integration_spec.rb index d26be56a..7fea2420 100644 --- a/spec/nylas/handler/http_client_integration_spec.rb +++ b/spec/nylas/handler/http_client_integration_spec.rb @@ -75,6 +75,46 @@ class TestHttpClientIntegration end end + describe "Integration Tests - Content-Type for body-less requests (HTTParty fix)" do + it "sends Content-Type: application/json for POST with empty payload" do + stub_request(:post, "https://test.api.nylas.com/v3/connect/revoke") + .with( + body: "{}", + headers: { "Content-Type" => "application/json" } + ) + .to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" }) + + http_client.send(:execute, + method: :post, + path: "https://test.api.nylas.com/v3/connect/revoke", + timeout: 30, + payload: {}, + api_key: "fake-key") + + expect(WebMock).to have_requested(:post, "https://test.api.nylas.com/v3/connect/revoke") + .with(headers: { "Content-Type" => "application/json" }, body: "{}") + end + + it "sends Content-Type: application/json for DELETE with empty payload" do + stub_request(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123") + .with( + body: "{}", + headers: { "Content-Type" => "application/json" } + ) + .to_return(status: 200, body: "{}", headers: { "Content-Type" => "application/json" }) + + http_client.send(:execute, + method: :delete, + path: "https://test.api.nylas.com/v3/scheduling/bookings/booking-123", + timeout: 30, + payload: {}, + api_key: "fake-key") + + expect(WebMock).to have_requested(:delete, "https://test.api.nylas.com/v3/scheduling/bookings/booking-123") + .with(headers: { "Content-Type" => "application/json" }, body: "{}") + end + end + describe "Integration Tests - backwards compatibility" do it "maintains the same response format as rest-client" do response_json = { "data" => { "id" => "123", "name" => "test" } } diff --git a/spec/nylas/handler/http_client_spec.rb b/spec/nylas/handler/http_client_spec.rb index a5d79ff8..9edb9c23 100644 --- a/spec/nylas/handler/http_client_spec.rb +++ b/spec/nylas/handler/http_client_spec.rb @@ -126,6 +126,17 @@ class TestHttpClient end context "when building request with a payload" do + it "returns the correct request with empty json payload and sets Content-Type" do + payload = {} + request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo", + payload: payload, api_key: "fake-key") + + expect(request[:method]).to eq(:post) + expect(request[:url]).to eq("https://test.api.nylas.com/foo") + expect(request[:payload]).to eq("{}") + expect(request[:headers]).to include("Content-type" => "application/json") + end + it "returns the correct request with a json payload" do payload = { foo: "bar" } request = http_client.send(:build_request, method: :post, path: "https://test.api.nylas.com/foo", diff --git a/spec/nylas/resources/bookings_spec.rb b/spec/nylas/resources/bookings_spec.rb index 79311258..5205a3d2 100644 --- a/spec/nylas/resources/bookings_spec.rb +++ b/spec/nylas/resources/bookings_spec.rb @@ -185,7 +185,7 @@ booking_id = "booking-123" path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" allow(bookings).to receive(:delete) - .with(path: path, query_params: nil) + .with(path: path, query_params: nil, request_body: nil) .and_return(delete_response) bookings_response = bookings.destroy(booking_id: booking_id) @@ -197,11 +197,26 @@ query_params = { "foo": "bar" } path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" allow(bookings).to receive(:delete) - .with(path: path, query_params: query_params) + .with(path: path, query_params: query_params, request_body: nil) .and_return(delete_response) bookings_response = bookings.destroy(booking_id: booking_id, query_params: query_params) expect(bookings_response).to eq(delete_response) end + + it "calls the delete method with request_body for cancellation_reason" do + booking_id = "booking-123" + request_body = { cancellation_reason: "Meeting no longer needed" } + path = "#{api_uri}/v3/scheduling/bookings/#{booking_id}" + allow(bookings).to receive(:delete) + .with(path: path, query_params: nil, request_body: request_body) + .and_return(delete_response) + + bookings_response = bookings.destroy( + booking_id: booking_id, + request_body: request_body + ) + expect(bookings_response).to eq(delete_response) + end end end