From aacf9d945230d639bf13d5b4148b7ee2970f9c5f Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 15:43:01 +0200
Subject: [PATCH 1/9] Move http errors to new seam/errors.rb

---
 lib/seam.rb                      |  1 +
 lib/seam/errors.rb               | 31 ++++++++++++++++++++++++
 lib/seam/request.rb              | 41 +++-----------------------------
 spec/seam_client/request_spec.rb |  6 ++---
 4 files changed, 38 insertions(+), 41 deletions(-)
 create mode 100644 lib/seam/errors.rb

diff --git a/lib/seam.rb b/lib/seam.rb
index ba15d9f6..1cc6d74b 100644
--- a/lib/seam.rb
+++ b/lib/seam.rb
@@ -8,6 +8,7 @@
 require_relative "seam/client"
 require_relative "seam/base_client"
 require_relative "seam/base_resource"
+require_relative "seam/errors"
 
 require_relative "seam/routes/resources/index"
 require_relative "seam/routes/clients/index"
diff --git a/lib/seam/errors.rb b/lib/seam/errors.rb
new file mode 100644
index 00000000..106c2fab
--- /dev/null
+++ b/lib/seam/errors.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Seam
+  module Errors
+    # HTTP
+    class SeamHttpApiError < StandardError
+      attr_reader :code, :status_code, :request_id, :data
+
+      def initialize(error, status_code, request_id)
+        super(error[:message])
+        @code = error[:type]
+        @status_code = status_code
+        @request_id = request_id
+        @data = error[:data]
+      end
+    end
+
+    class SeamHttpUnauthorizedError < SeamHttpApiError
+      def initialize(request_id)
+        super({type: "unauthorized", message: "Unauthorized"}, 401, request_id)
+      end
+    end
+
+    class SeamHttpInvalidInputError < SeamHttpApiError
+      def initialize(error, status_code, request_id)
+        super(error, status_code, request_id)
+        @code = "invalid_input"
+      end
+    end
+  end
+end
diff --git a/lib/seam/request.rb b/lib/seam/request.rb
index cd06b2d8..08c5eef4 100644
--- a/lib/seam/request.rb
+++ b/lib/seam/request.rb
@@ -6,16 +6,6 @@ module Seam
   class Request
     attr_reader :endpoint, :debug
 
-    class Error < StandardError
-      attr_reader :status, :response
-
-      def initialize(message, status, response)
-        super(message)
-        @status = status
-        @response = response
-      end
-    end
-
     def initialize(auth_headers:, endpoint:, debug: false)
       @auth_headers = auth_headers
       @endpoint = endpoint
@@ -44,7 +34,7 @@ def handle_error_response(response, _method, _uri)
       status_code = response.status.code
       request_id = response.headers["seam-request-id"]
 
-      raise SeamHttpUnauthorizedError.new(request_id) if status_code == 401
+      raise Errors::SeamHttpUnauthorizedError.new(request_id) if status_code == 401
 
       error = response.parse["error"] || {}
       error_type = error["type"] || "unknown_error"
@@ -54,9 +44,9 @@ def handle_error_response(response, _method, _uri)
         data: error["data"]
       }
 
-      raise SeamHttpInvalidInputError.new(error_details, status_code, request_id) if error_type == "invalid_input"
+      raise Errors::SeamHttpInvalidInputError.new(error_details, status_code, request_id) if error_type == "invalid_input"
 
-      raise SeamHttpApiError.new(error_details, status_code, request_id)
+      raise Errors::SeamHttpApiError.new(error_details, status_code, request_id)
     end
 
     def build_url(uri)
@@ -77,29 +67,4 @@ def user_agent
       "seam-ruby/#{Seam::VERSION}"
     end
   end
-
-  class SeamHttpApiError < StandardError
-    attr_reader :code, :status_code, :request_id, :data
-
-    def initialize(error, status_code, request_id)
-      super(error[:message])
-      @code = error[:type]
-      @status_code = status_code
-      @request_id = request_id
-      @data = error[:data]
-    end
-  end
-
-  class SeamHttpUnauthorizedError < SeamHttpApiError
-    def initialize(request_id)
-      super({type: "unauthorized", message: "Unauthorized"}, 401, request_id)
-    end
-  end
-
-  class SeamHttpInvalidInputError < SeamHttpApiError
-    def initialize(error, status_code, request_id)
-      super(error, status_code, request_id)
-      @code = "invalid_input"
-    end
-  end
 end
diff --git a/spec/seam_client/request_spec.rb b/spec/seam_client/request_spec.rb
index 9ef5803f..50942547 100644
--- a/spec/seam_client/request_spec.rb
+++ b/spec/seam_client/request_spec.rb
@@ -12,7 +12,7 @@
       end
 
       it "raises SeamHttpUnauthorizedError" do
-        expect { seam.devices.list }.to raise_error(Seam::SeamHttpUnauthorizedError) do |error|
+        expect { seam.devices.list }.to raise_error(Seam::Errors::SeamHttpUnauthorizedError) do |error|
           expect(error.message).to eq("Unauthorized")
           expect(error.request_id).to eq(request_id)
         end
@@ -37,7 +37,7 @@
       end
 
       it "raises SeamHttpInvalidInputError" do
-        expect { seam.devices.get(device_id: "invalid_device_id") }.to raise_error(Seam::SeamHttpInvalidInputError) do |error|
+        expect { seam.devices.get(device_id: "invalid_device_id") }.to raise_error(Seam::Errors::SeamHttpInvalidInputError) do |error|
           expect(error.message).to eq(error_message)
           expect(error.status_code).to eq(error_status)
           expect(error.request_id).to eq(request_id)
@@ -64,7 +64,7 @@
       end
 
       it "raises SeamHttpApiError with the correct details" do
-        expect { seam.devices.list }.to raise_error(Seam::SeamHttpApiError) do |error|
+        expect { seam.devices.list }.to raise_error(Seam::Errors::SeamHttpApiError) do |error|
           expect(error.message).to eq(error_message)
           expect(error.status_code).to eq(error_status)
           expect(error.request_id).to eq(request_id)

From c4e45b30652404078a322da63927172e9e4c7ba1 Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 15:47:35 +0200
Subject: [PATCH 2/9] Add action attempt errors and use them

---
 lib/seam/errors.rb                     | 30 ++++++++++++++++++++++++++
 lib/seam/utils/action_attempt_utils.rb |  4 ++--
 2 files changed, 32 insertions(+), 2 deletions(-)

diff --git a/lib/seam/errors.rb b/lib/seam/errors.rb
index 106c2fab..b20165ff 100644
--- a/lib/seam/errors.rb
+++ b/lib/seam/errors.rb
@@ -27,5 +27,35 @@ def initialize(error, status_code, request_id)
         @code = "invalid_input"
       end
     end
+
+    # Action attempt
+    class SeamActionAttemptError < StandardError
+      attr_reader :action_attempt
+
+      def initialize(message, action_attempt)
+        super(message)
+        @action_attempt = action_attempt
+      end
+
+      def name
+        self.class.name
+      end
+    end
+
+    class SeamActionAttemptFailedError < SeamActionAttemptError
+      attr_reader :code
+
+      def initialize(action_attempt)
+        super(action_attempt.error.message, action_attempt)
+        @code = action_attempt.error.type
+      end
+    end
+
+    class SeamActionAttemptTimeoutError < SeamActionAttemptError
+      def initialize(action_attempt, timeout)
+        message = "Timed out waiting for action attempt after #{timeout}s"
+        super(message, action_attempt)
+      end
+    end
   end
 end
diff --git a/lib/seam/utils/action_attempt_utils.rb b/lib/seam/utils/action_attempt_utils.rb
index 202a918d..248fa8eb 100644
--- a/lib/seam/utils/action_attempt_utils.rb
+++ b/lib/seam/utils/action_attempt_utils.rb
@@ -21,11 +21,11 @@ def self.wait_until_finished(action_attempt, client, timeout: 5.0, polling_inter
           sleep(polling_interval)
           time_waiting += polling_interval
 
-          raise "Timed out waiting for action attempt to be finished" if time_waiting > timeout
+          raise Errors::SeamActionAttemptTimeoutError.new(action_attempt, timeout) if time_waiting > timeout
 
           action_attempt = update_action_attempt(action_attempt, client)
 
-          raise "Action Attempt failed: #{action_attempt.error["message"]}" if action_attempt.status == "failed"
+          raise Errors::SeamActionAttemptFailedError.new(action_attempt) if action_attempt.status == "failed"
         end
 
         action_attempt

From 53d08e22c933b3597dba262758c5e254b833c7bc Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 15:48:37 +0200
Subject: [PATCH 3/9] Correctly handle error action attempts

---
 lib/seam/utils/action_attempt_utils.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/seam/utils/action_attempt_utils.rb b/lib/seam/utils/action_attempt_utils.rb
index 248fa8eb..0c88a5ab 100644
--- a/lib/seam/utils/action_attempt_utils.rb
+++ b/lib/seam/utils/action_attempt_utils.rb
@@ -25,7 +25,7 @@ def self.wait_until_finished(action_attempt, client, timeout: 5.0, polling_inter
 
           action_attempt = update_action_attempt(action_attempt, client)
 
-          raise Errors::SeamActionAttemptFailedError.new(action_attempt) if action_attempt.status == "failed"
+          raise Errors::SeamActionAttemptFailedError.new(action_attempt) if action_attempt.status == "error"
         end
 
         action_attempt

From ff7b257283b482fae8fe539f0caa0eed1d5e7f96 Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 16:23:14 +0200
Subject: [PATCH 4/9] Ensure default values for timeout and polling_interval
 are used in wait_until_finished

---
 lib/seam/utils/action_attempt_utils.rb | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/seam/utils/action_attempt_utils.rb b/lib/seam/utils/action_attempt_utils.rb
index 0c88a5ab..8cd3d305 100644
--- a/lib/seam/utils/action_attempt_utils.rb
+++ b/lib/seam/utils/action_attempt_utils.rb
@@ -14,7 +14,10 @@ def self.decide_and_wait(action_attempt, client, wait_for_action_attempt)
         end
       end
 
-      def self.wait_until_finished(action_attempt, client, timeout: 5.0, polling_interval: 0.5)
+      def self.wait_until_finished(action_attempt, client, timeout: nil, polling_interval: nil)
+        timeout = timeout.nil? ? 5.0 : timeout
+        polling_interval = polling_interval.nil? ? 0.5 : polling_interval
+
         time_waiting = 0.0
 
         while action_attempt.status == "pending"

From d0e3b57e8680f9bd71cb7d21321c0afad51b62e8 Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 17:33:14 +0200
Subject: [PATCH 5/9] Default wait_for_action_attempt client setting to true

---
 lib/seam/client.rb                 | 2 +-
 spec/seam_client/init_seam_spec.rb | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/seam/client.rb b/lib/seam/client.rb
index c6b64735..e9ff5323 100644
--- a/lib/seam/client.rb
+++ b/lib/seam/client.rb
@@ -10,7 +10,7 @@ class Client
     attr_accessor :wait_for_action_attempt, :defaults
 
     def initialize(api_key: nil, personal_access_token: nil, workspace_id: nil, endpoint: nil,
-      wait_for_action_attempt: false, debug: false)
+      wait_for_action_attempt: true, debug: false)
       options = SeamOptions.parse_options(api_key: api_key, personal_access_token: personal_access_token, workspace_id: workspace_id, endpoint: endpoint)
       @endpoint = options[:endpoint]
       @auth_headers = options[:auth_headers]
diff --git a/spec/seam_client/init_seam_spec.rb b/spec/seam_client/init_seam_spec.rb
index bea8aed2..e6c72e26 100644
--- a/spec/seam_client/init_seam_spec.rb
+++ b/spec/seam_client/init_seam_spec.rb
@@ -6,7 +6,7 @@
   describe "#initialize" do
     it "initializes Seam with fixture" do
       expect(seam.lts_version).not_to be_nil
-      expect(seam.wait_for_action_attempt).to be_falsey
+      expect(seam.wait_for_action_attempt).to be true
     end
   end
 end

From 5c16aeb858621a21e38360aab76c30aecc0292dd Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 17:34:39 +0200
Subject: [PATCH 6/9] Fix error action attempt handling

---
 lib/seam/utils/action_attempt_utils.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/seam/utils/action_attempt_utils.rb b/lib/seam/utils/action_attempt_utils.rb
index 8cd3d305..fa3ab4f1 100644
--- a/lib/seam/utils/action_attempt_utils.rb
+++ b/lib/seam/utils/action_attempt_utils.rb
@@ -27,10 +27,10 @@ def self.wait_until_finished(action_attempt, client, timeout: nil, polling_inter
           raise Errors::SeamActionAttemptTimeoutError.new(action_attempt, timeout) if time_waiting > timeout
 
           action_attempt = update_action_attempt(action_attempt, client)
-
-          raise Errors::SeamActionAttemptFailedError.new(action_attempt) if action_attempt.status == "error"
         end
 
+        raise Errors::SeamActionAttemptFailedError.new(action_attempt) if action_attempt.status == "error"
+
         action_attempt
       end
 

From 17ab5a19f3cfa49ec4b97f62ba372f1a6afaa2d3 Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Thu, 25 Jul 2024 17:34:59 +0200
Subject: [PATCH 7/9] Add wait_for_action_attepmt_spec.rb

---
 spec/resources/action_attempt_spec.rb         | 23 ------------------
 .../wait_for_action_attepmt_spec.rb           | 24 +++++++++++++++++++
 2 files changed, 24 insertions(+), 23 deletions(-)
 create mode 100644 spec/seam_client/wait_for_action_attepmt_spec.rb

diff --git a/spec/resources/action_attempt_spec.rb b/spec/resources/action_attempt_spec.rb
index d30dee51..d58bc557 100644
--- a/spec/resources/action_attempt_spec.rb
+++ b/spec/resources/action_attempt_spec.rb
@@ -73,29 +73,6 @@
     it "returns an updated ActionAttempt" do
       expect(result.status).to eq(finished_status)
     end
-
-    context "when action attempt fails" do
-      let(:status) { "failed" }
-      let(:error_message) { "Something went wrong" }
-
-      before do
-        stub_seam_request(
-          :post,
-          "/action_attempts/get",
-          {action_attempt: action_attempt_hash.merge(status: status, error: {"message" => error_message})}
-        )
-      end
-
-      it "raises an error" do
-        expect { described_class.wait_until_finished(action_attempt, client) }.to raise_error("Action Attempt failed: #{error_message}")
-      end
-    end
-
-    context "when timeout is reached" do
-      it "raises a timeout error" do
-        expect { described_class.wait_until_finished(action_attempt, client, timeout: 0.1) }.to raise_error("Timed out waiting for action attempt to be finished")
-      end
-    end
   end
 
   describe ".update_action_attempt" do
diff --git a/spec/seam_client/wait_for_action_attepmt_spec.rb b/spec/seam_client/wait_for_action_attepmt_spec.rb
new file mode 100644
index 00000000..5fd45180
--- /dev/null
+++ b/spec/seam_client/wait_for_action_attepmt_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+RSpec.describe Seam::Client do
+  let(:client) { described_class.new(api_key: "seam_apikey1_token") }
+  let(:device_id) { "august_device_1" }
+
+  describe "error handling" do
+    it "raises SeamActionAttemptFailedError on failure" do
+      stub_seam_request(:post, "/locks/unlock_door", {
+        action_attempt: {
+          action_attempt_id: "1234",
+          status: "error",
+          error: {type: "device_disconnected", message: "Device is disconnected"}
+        }
+      }).with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      expect {
+        client.locks.unlock_door(device_id: device_id)
+      }.to raise_error(Seam::Errors::SeamActionAttemptFailedError)
+    end
+  end
+end

From 000ba6bc560f17b661037c2930221a2a3876924a Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Fri, 26 Jul 2024 12:39:44 +0200
Subject: [PATCH 8/9] Fix spec/request_spec.rb

---
 spec/request_spec.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/spec/request_spec.rb b/spec/request_spec.rb
index b8cebaaf..33cca679 100644
--- a/spec/request_spec.rb
+++ b/spec/request_spec.rb
@@ -22,7 +22,7 @@
 
       it "parses the error" do
         expect { client.health }.to raise_error do |error|
-          expect(error).to be_a(Seam::SeamHttpApiError)
+          expect(error).to be_a(Seam::Errors::SeamHttpApiError)
           expect(error.message).to eq(message)
           expect(error.code).to eq(type)
           expect(error.request_id).to eq(request_id)
@@ -48,7 +48,7 @@
 
       it "parses the error" do
         expect { client.health }.to raise_error do |error|
-          expect(error).to be_a(Seam::SeamHttpApiError)
+          expect(error).to be_a(Seam::Errors::SeamHttpApiError)
           expect(error.message).to eq(message)
           expect(error.code).to eq(type)
           expect(error.request_id).to eq(request_id)

From 0e79fcc12f885329217355ddb4bef283f364261e Mon Sep 17 00:00:00 2001
From: Andrii Balitskyi <10balian10@gmail.com>
Date: Fri, 26 Jul 2024 13:42:50 +0200
Subject: [PATCH 9/9] Improve spec/seam_client/wait_for_action_attepmt_spec.rb

---
 .../wait_for_action_attepmt_spec.rb           | 168 +++++++++++++++++-
 1 file changed, 160 insertions(+), 8 deletions(-)

diff --git a/spec/seam_client/wait_for_action_attepmt_spec.rb b/spec/seam_client/wait_for_action_attepmt_spec.rb
index 5fd45180..59d0d0bf 100644
--- a/spec/seam_client/wait_for_action_attepmt_spec.rb
+++ b/spec/seam_client/wait_for_action_attepmt_spec.rb
@@ -3,22 +3,174 @@
 require "spec_helper"
 
 RSpec.describe Seam::Client do
-  let(:client) { described_class.new(api_key: "seam_apikey1_token") }
+  let(:api_key) { "seam_apikey1_token" }
   let(:device_id) { "august_device_1" }
 
-  describe "error handling" do
-    it "raises SeamActionAttemptFailedError on failure" do
-      stub_seam_request(:post, "/locks/unlock_door", {
+  describe "action attempt handling" do
+    let(:success_response) do
+      {
+        action_attempt: {
+          action_attempt_id: "1234",
+          status: "success"
+        }
+      }
+    end
+
+    let(:pending_response) do
+      {
+        action_attempt: {
+          action_attempt_id: "1234",
+          status: "pending"
+        }
+      }
+    end
+
+    let(:error_response) do
+      {
         action_attempt: {
           action_attempt_id: "1234",
           status: "error",
-          error: {type: "device_disconnected", message: "Device is disconnected"}
+          error: {message: "Failed", type: "foo"}
         }
-      }).with { |req| req.body.source == {device_id: device_id}.to_json }
+      }
+    end
+
+    it "waits for action attempt when specified on method call" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", success_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id, wait_for_action_attempt: true)
+      expect(action_attempt.status).to eq("success")
+    end
+
+    it "waits for action attempt by default" do
+      client = described_class.new(api_key: api_key)
+
+      stub_seam_request(:post, "/locks/unlock_door", success_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("success")
+    end
+
+    it "doesn't wait for action attempt when set to false in client initialization" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", pending_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("pending")
+    end
+
+    it "can set class default with an object in client initialization" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: {timeout: 5})
+
+      stub_seam_request(:post, "/locks/unlock_door", success_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("success")
+    end
+
+    it "returns successful action attempt" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", pending_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("pending")
+
+      stub_seam_request(:post, "/action_attempts/get", success_response)
+        .with { |req| req.body.source == {action_attempt_id: "1234"}.to_json }
+
+      successful_action_attempt = client.action_attempts.get(
+        action_attempt_id: action_attempt.action_attempt_id
+      )
+
+      expect(successful_action_attempt.status).to eq("success")
+
+      resolved_action_attempt = client.action_attempts.get(
+        action_attempt_id: action_attempt.action_attempt_id,
+        wait_for_action_attempt: true
+      )
+
+      expect(resolved_action_attempt.action_attempt_id).to eq(successful_action_attempt.action_attempt_id)
+      expect(resolved_action_attempt.status).to eq(successful_action_attempt.status)
+    end
+
+    it "times out when waiting for action attempt" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", pending_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("pending")
+
+      stub_seam_request(:post, "/action_attempts/get", pending_response)
+        .with { |req| req.body.source == {action_attempt_id: "1234"}.to_json }
+
+      expect {
+        client.action_attempts.get(
+          action_attempt_id: action_attempt.action_attempt_id,
+          wait_for_action_attempt: {timeout: 0.1}
+        )
+      }.to raise_error(Seam::Errors::SeamActionAttemptTimeoutError) do |error|
+        expect(error.action_attempt.action_attempt_id).to eq(action_attempt.action_attempt_id)
+        expect(error.action_attempt.status).to eq(action_attempt.status)
+      end
+    end
+
+    it "rejects when action attempt fails" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", pending_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("pending")
+
+      stub_seam_request(:post, "/action_attempts/get", error_response)
+        .with { |req| req.body.source == {action_attempt_id: "1234"}.to_json }
+
+      expect {
+        client.action_attempts.get(
+          action_attempt_id: action_attempt.action_attempt_id,
+          wait_for_action_attempt: true
+        )
+      }.to raise_error(Seam::Errors::SeamActionAttemptFailedError) do |error|
+        expect(error.message).to include("Failed")
+        expect(error.action_attempt.action_attempt_id).to eq(action_attempt.action_attempt_id)
+        expect(error.action_attempt.status).to eq("error")
+        expect(error.code).to eq("foo")
+      end
+    end
+
+    it "times out if waiting for polling interval" do
+      client = described_class.new(api_key: api_key, wait_for_action_attempt: false)
+
+      stub_seam_request(:post, "/locks/unlock_door", pending_response)
+        .with { |req| req.body.source == {device_id: device_id}.to_json }
+
+      action_attempt = client.locks.unlock_door(device_id: device_id)
+      expect(action_attempt.status).to eq("pending")
+
+      stub_seam_request(:post, "/action_attempts/get", pending_response)
+        .with { |req| req.body.source == {action_attempt_id: "1234"}.to_json }
 
       expect {
-        client.locks.unlock_door(device_id: device_id)
-      }.to raise_error(Seam::Errors::SeamActionAttemptFailedError)
+        client.action_attempts.get(
+          action_attempt_id: action_attempt.action_attempt_id,
+          wait_for_action_attempt: {timeout: 0.5, polling_interval: 3}
+        )
+      }.to raise_error(Seam::Errors::SeamActionAttemptTimeoutError) do |error|
+        expect(error.action_attempt.action_attempt_id).to eq(action_attempt.action_attempt_id)
+        expect(error.action_attempt.status).to eq(action_attempt.status)
+      end
     end
   end
 end
