From a9ff86b80a95e767813c9d447b4981854be8671f Mon Sep 17 00:00:00 2001 From: vbiletskii Date: Tue, 19 Aug 2025 16:04:09 +0300 Subject: [PATCH 1/3] Add rate limit error --- lib/teamtailor/error.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/teamtailor/error.rb b/lib/teamtailor/error.rb index d375fdd..930962f 100644 --- a/lib/teamtailor/error.rb +++ b/lib/teamtailor/error.rb @@ -14,6 +14,8 @@ def self.from_response(body:, status:) Teamtailor::InvalidApiVersionError.new(error_message) when 422 Teamtailor::UnprocessableEntityError.new(error_message) + when 429 + RateLimitError.new when 400..499 ClientError.new(error_message) when 500..599 @@ -48,5 +50,7 @@ class UnloadedRelationError < ClientError; end class UnprocessableEntityError < ClientError; end + class RateLimitError < ClientError; end + class UnknownResponseError < Error; end end From cdd060460fc9aa67d3a70760a98719d9220993e8 Mon Sep 17 00:00:00 2001 From: vbiletskii Date: Tue, 16 Sep 2025 10:53:47 +0300 Subject: [PATCH 2/3] Add spec --- spec/teamtailor/error_spec.rb | 118 ++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 spec/teamtailor/error_spec.rb diff --git a/spec/teamtailor/error_spec.rb b/spec/teamtailor/error_spec.rb new file mode 100644 index 0000000..3c0fea3 --- /dev/null +++ b/spec/teamtailor/error_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +RSpec.describe Teamtailor::Error do + describe ".from_response" do + subject(:error) { described_class.from_response(body: body, status: status) } + + let(:error_detail) { "Something went wrong" } + let(:body) { { errors: [{ detail: error_detail }] }.to_json } + + context "when status is 401" do + let(:status) { 401 } + + it "returns an UnauthorizedRequestError" do + expect(error).to be_a(Teamtailor::UnauthorizedRequestError) + end + end + + context "when status is 406" do + let(:status) { 406 } + + it "returns an InvalidApiVersionError with message" do + expect(error).to be_a(Teamtailor::InvalidApiVersionError) + expect(error.message).to eq(error_detail) + end + end + + context "when status is 422" do + let(:status) { 422 } + + it "returns an UnprocessableEntityError with message" do + expect(error).to be_a(Teamtailor::UnprocessableEntityError) + expect(error.message).to eq(error_detail) + end + end + + context "when status is 429" do + let(:status) { 429 } + + it "returns a RateLimitError" do + expect(error).to be_a(Teamtailor::RateLimitError) + end + end + + context "when status is between 400 and 499 (other)" do + let(:status) { 418 } + + it "returns a ClientError with message" do + expect(error).to be_a(Teamtailor::ClientError) + expect(error.message).to eq(error_detail) + end + end + + context "when status is between 500 and 599" do + let(:status) { 500 } + + it "returns a ServerError with message" do + expect(error).to be_a(Teamtailor::ServerError) + expect(error.message).to eq(error_detail) + end + end + + context "when status is unknown" do + let(:status) { 300 } + + it "returns an UnknownResponseError with status in message" do + expect(error).to be_a(Teamtailor::UnknownResponseError) + expect(error.message).to include("Unexpected error (status: 300)") + end + end + + context "when body does not parse as JSON" do + let(:status) { 422 } + let(:body) { "not-json" } + + it "defaults error message to Unknown error" do + expect(error).to be_a(Teamtailor::UnprocessableEntityError) + expect(error.message).to eq("Unknown error") + end + end + + context "when body has a different error structure" do + let(:status) { 422 } + let(:body) { { errors: { detail: "Another error" } }.to_json } + + it "uses the alternative error detail" do + expect(error.message).to eq("Another error") + end + end + end + + describe ".parse_body" do + subject(:parsed) { described_class.parse_body(body) } + + context "with valid JSON" do + let(:body) { { test: "ok" }.to_json } + + it "parses and returns the hash" do + expect(parsed).to eq("test" => "ok") + end + end + + context "with invalid JSON" do + let(:body) { "this is not json" } + + it "returns an empty hash" do + expect(parsed).to eq({}) + end + end + + context "with empty string" do + let(:body) { "" } + + it "returns an empty hash" do + expect(parsed).to eq({}) + end + end + end +end From d80b7343693e920436a6ddc3ab8e10bd103676ff Mon Sep 17 00:00:00 2001 From: vbiletskii Date: Tue, 16 Sep 2025 11:00:16 +0300 Subject: [PATCH 3/3] Use Ruby Standard Style --- spec/teamtailor/error_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/teamtailor/error_spec.rb b/spec/teamtailor/error_spec.rb index 3c0fea3..b44e912 100644 --- a/spec/teamtailor/error_spec.rb +++ b/spec/teamtailor/error_spec.rb @@ -5,7 +5,7 @@ subject(:error) { described_class.from_response(body: body, status: status) } let(:error_detail) { "Something went wrong" } - let(:body) { { errors: [{ detail: error_detail }] }.to_json } + let(:body) { {errors: [{detail: error_detail}]}.to_json } context "when status is 401" do let(:status) { 401 } @@ -80,7 +80,7 @@ context "when body has a different error structure" do let(:status) { 422 } - let(:body) { { errors: { detail: "Another error" } }.to_json } + let(:body) { {errors: {detail: "Another error"}}.to_json } it "uses the alternative error detail" do expect(error.message).to eq("Another error") @@ -92,7 +92,7 @@ subject(:parsed) { described_class.parse_body(body) } context "with valid JSON" do - let(:body) { { test: "ok" }.to_json } + let(:body) { {test: "ok"}.to_json } it "parses and returns the hash" do expect(parsed).to eq("test" => "ok")