From 018595e775c2a6cc313248c5d892486d03c1f837 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 04:44:55 +0000 Subject: [PATCH 1/2] test: restore coverage for malformed error responses PR #537 deleted spec/seam_client/request_spec.rb and claimed its coverage was folded into http_error_spec against the fake. That was only partly true: the fake's simulated outage responds with no content type and an empty body, so of the branches in seam_api_error_response? only the content type guard stayed covered. The malformed JSON, JSON without an error object, and error object without string fields branches all lost coverage. Restore them with WebMock, which is the right tool here for the same reason it backs the headers and retry specs: the fake cannot produce these responses. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB --- spec/seam_client/malformed_response_spec.rb | 51 +++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 spec/seam_client/malformed_response_spec.rb diff --git a/spec/seam_client/malformed_response_spec.rb b/spec/seam_client/malformed_response_spec.rb new file mode 100644 index 0000000..86f6987 --- /dev/null +++ b/spec/seam_client/malformed_response_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# The fake cannot produce malformed error responses, so WebMock drives the +# bodies that must fall through seam_api_error_response? to Faraday's own +# error handling rather than being parsed into a Seam::Http::ApiError. +RSpec.describe Seam::Http::Request do + let(:seam) { Seam.new(api_key: "seam_some_api_key") } + let(:url) { "#{Seam::DEFAULT_ENDPOINT}/devices/list" } + + describe "non-Seam error responses" do + it "raises a Faraday error for a plain text response" do + stub_request(:post, url).to_return( + status: 500, + body: "Internal Server Error", + headers: {"Content-Type" => "text/plain"} + ) + + expect { seam.devices.list }.to raise_error(Faraday::ServerError) + end + + it "raises a Faraday error for malformed JSON" do + stub_request(:post, url).to_return( + status: 500, + body: "{invalid json", + headers: {"Content-Type" => "application/json"} + ) + + expect { seam.devices.list }.to raise_error(Faraday::ServerError) + end + + it "raises a Faraday error for JSON without an error object" do + stub_request(:post, url).to_return( + status: 500, + body: {message: "Some error"}.to_json, + headers: {"Content-Type" => "application/json"} + ) + + expect { seam.devices.list }.to raise_error(Faraday::ServerError) + end + + it "raises a Faraday error for an error object without a type and message" do + stub_request(:post, url).to_return( + status: 500, + body: {error: {code: 500}}.to_json, + headers: {"Content-Type" => "application/json"} + ) + + expect { seam.devices.list }.to raise_error(Faraday::ServerError) + end + end +end From 8556b6da42107dbda46382a77a266b1b9d870bd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 04:44:55 +0000 Subject: [PATCH 2/2] test: cover resource error and warning coercion PR #537 deleted the per-route client specs as generated-code tests, but three of them were the only coverage of ResourceErrorsSupport and ResourceWarningsSupport, the hand-maintained modules that convert error and warning hashes into ResourceError and ResourceWarning objects with parsed dates. Cover that behavior at the resource level, where it lives, including the empty default when a resource carries no errors or warnings. Also cover DeepHashAccessor wrapping hashes inside arrays, which the deleted specs exercised indirectly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011DzapiU8A9NMdyoTybL9xB --- spec/deep_hash_accessor_spec.rb | 8 +++- spec/resources/resource_errors_spec.rb | 57 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 spec/resources/resource_errors_spec.rb diff --git a/spec/deep_hash_accessor_spec.rb b/spec/deep_hash_accessor_spec.rb index b066e18..3f45f16 100644 --- a/spec/deep_hash_accessor_spec.rb +++ b/spec/deep_hash_accessor_spec.rb @@ -12,7 +12,8 @@ city: "San Francisco", country: "USA" }, - hobbies: %w[reading cycling] + hobbies: %w[reading cycling], + pets: [{name: "Rex", species: "dog"}] } end @@ -36,6 +37,11 @@ it "returns arrays as is for simple types" do expect(accessor.hobbies).to eq(%w[reading cycling]) end + + it "wraps hashes inside arrays" do + expect(accessor.pets.first).to be_a(described_class) + expect(accessor.pets.first.name).to eq("Rex") + end end describe "non-existent keys" do diff --git a/spec/resources/resource_errors_spec.rb b/spec/resources/resource_errors_spec.rb new file mode 100644 index 0000000..47f47f1 --- /dev/null +++ b/spec/resources/resource_errors_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.describe "resource errors and warnings" do + describe Seam::Resources::ResourceErrorsSupport do + it "converts error hashes into ResourceError objects" do + device = Seam::Resources::Device.load_from_response( + "device_id" => "device_id_1234", + "errors" => [ + { + "error_code" => "device_removed", + "message" => "Device was removed", + "created_at" => "2024-01-01T00:00:00Z" + } + ] + ) + + error = device.errors.first + expect(error).to be_a(Seam::Resources::ResourceError) + expect(error.error_code).to eq("device_removed") + expect(error.message).to eq("Device was removed") + expect(error.created_at).to be_a(Time) + end + + it "returns an empty array when the resource has no errors" do + device = Seam::Resources::Device.load_from_response("device_id" => "device_id_1234") + + expect(device.errors).to eq([]) + end + end + + describe Seam::Resources::ResourceWarningsSupport do + it "converts warning hashes into ResourceWarning objects" do + device = Seam::Resources::Device.load_from_response( + "device_id" => "device_id_1234", + "warnings" => [ + { + "warning_code" => "privacy_mode", + "message" => "Device is in privacy mode", + "created_at" => "2024-01-01T00:00:00Z" + } + ] + ) + + warning = device.warnings.first + expect(warning).to be_a(Seam::Resources::ResourceWarning) + expect(warning.warning_code).to eq("privacy_mode") + expect(warning.message).to eq("Device is in privacy mode") + expect(warning.created_at).to be_a(Time) + end + + it "returns an empty array when the resource has no warnings" do + device = Seam::Resources::Device.load_from_response("device_id" => "device_id_1234") + + expect(device.warnings).to eq([]) + end + end +end