From 141118017f7fc69b9a5f878d813c0ab3c3c33b54 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Thu, 26 Mar 2026 00:31:59 -0700 Subject: [PATCH 1/2] Add webhooks support (create, list, delete) Adds Webhooks service and Webhook model, plus DELETE HTTP method support in Request class and 204 No Content handling in response parser. Updates feature matrix. --- README.md | 37 +++++++++++++++-- lib/accessgrid.rb | 1 + lib/accessgrid/console.rb | 48 ++++++++++++++++++++++ lib/accessgrid/request.rb | 15 ++++--- spec/console_spec.rb | 86 +++++++++++++++++++++++++++++++++++++++ spec/request_spec.rb | 7 +++- 6 files changed, 185 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index ceae0d4..9b563e2 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,37 @@ puts "Card Template ID: #{response.card_template_identifier}" puts "Environment ID: #{response.environment_identifier}" ``` +### Webhooks + +#### Create a webhook + +```ruby +webhook = client.console.webhooks.create( + name: 'Production', + url: 'https://example.com/webhooks', + subscribed_events: ['ag.access_pass.issued'] +) + +puts "Webhook created: #{webhook.id}" +puts "Private key: #{webhook.private_key}" +``` + +#### List webhooks + +```ruby +webhooks = client.console.webhooks.list + +webhooks.each do |webhook| + puts "ID: #{webhook.id}, Name: #{webhook.name}" +end +``` + +#### Delete a webhook + +```ruby +client.console.webhooks.delete('abc123') +``` + ## Configuration The SDK can be configured with a custom API endpoint: @@ -329,9 +360,9 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/access | GET /v1/console/pass-template-pairs | `console.list_pass_template_pairs()` | Y | | POST /v1/console/card-templates/{id}/ios_preflight | `console.ios_preflight()` | Y | | GET /v1/console/ledger-items | `console.list_ledger_items()` / `console.ledger_items()` | Y | -| GET /v1/console/webhooks | `console.webhooks.list()` | - | -| POST /v1/console/webhooks | `console.webhooks.create()` | - | -| DELETE /v1/console/webhooks/{id} | `console.webhooks.delete()` | - | +| GET /v1/console/webhooks | `console.webhooks.list()` | Y | +| POST /v1/console/webhooks | `console.webhooks.create()` | Y | +| DELETE /v1/console/webhooks/{id} | `console.webhooks.delete()` | Y | | POST /v1/console/hid/orgs | `console.hid.orgs.create()` | - | | POST /v1/console/hid/orgs/activate | `console.hid.orgs.activate()` | - | | GET /v1/console/hid/orgs | `console.hid.orgs.list()` | - | diff --git a/lib/accessgrid.rb b/lib/accessgrid.rb index 81d0a9d..8aabf39 100644 --- a/lib/accessgrid.rb +++ b/lib/accessgrid.rb @@ -63,6 +63,7 @@ def execute_and_process_request!(request) def handle_response(response) case response.code.to_i + when 204 then {} when 200, 201, 202 then JSON.parse(response.body) when 401 then raise AuthenticationError, 'Invalid credentials' when 402 then raise Error, 'Insufficient account balance' diff --git a/lib/accessgrid/console.rb b/lib/accessgrid/console.rb index 88f8139..8cf07d1 100644 --- a/lib/accessgrid/console.rb +++ b/lib/accessgrid/console.rb @@ -4,8 +4,11 @@ module AccessGrid # Manages enterprise template and logging operations. class Console + attr_reader :webhooks + def initialize(client) @client = client + @webhooks = Webhooks.new(client) end def create_template(params) @@ -200,4 +203,49 @@ def initialize(data) @use_case = data['use_case'] end end + + # Manages webhook operations. + class Webhooks + def initialize(client) + @client = client + end + + def create(name:, url:, subscribed_events:, auth_method: 'bearer_token') + data = { + name: name, + url: url, + subscribed_events: subscribed_events, + auth_method: auth_method + } + response = @client.make_request(:post, '/v1/console/webhooks', data) + Webhook.new(response) + end + + def list(**params) + response = @client.make_request(:get, '/v1/console/webhooks', nil, params) + (response['webhooks'] || []).map { |wh| Webhook.new(wh) } + end + + def delete(webhook_id) + @client.make_request(:delete, "/v1/console/webhooks/#{webhook_id}") + end + end + + # Represents a webhook configuration. + class Webhook + attr_reader :id, :name, :url, :auth_method, :subscribed_events, + :created_at, :private_key, :client_cert, :cert_expires_at + + def initialize(data) + @id = data['id'] + @name = data['name'] + @url = data['url'] + @auth_method = data['auth_method'] + @subscribed_events = data['subscribed_events'] + @created_at = data['created_at'] + @private_key = data['private_key'] + @client_cert = data['client_cert'] + @cert_expires_at = data['cert_expires_at'] + end + end end diff --git a/lib/accessgrid/request.rb b/lib/accessgrid/request.rb index e863476..dfd6ea3 100644 --- a/lib/accessgrid/request.rb +++ b/lib/accessgrid/request.rb @@ -42,10 +42,11 @@ def net_http_request def generate_net_http_request! klass = case http_method - when :get then Net::HTTP::Get - when :post then Net::HTTP::Post - when :put then Net::HTTP::Put - when :patch then Net::HTTP::Patch + when :get then Net::HTTP::Get + when :post then Net::HTTP::Post + when :put then Net::HTTP::Put + when :patch then Net::HTTP::Patch + when :delete then Net::HTTP::Delete else raise ArgumentError, "Unsupported HTTP method: #{http_method}" end @@ -89,6 +90,10 @@ def get? http_method == :get end + def delete? + http_method == :delete + end + def post? http_method == :post end @@ -98,7 +103,7 @@ def empty_body? end def post_without_body_or_get? - get? || (post? && empty_body?) + get? || delete? || (post? && empty_body?) end def default_payload diff --git a/spec/console_spec.rb b/spec/console_spec.rb index 2d79017..1b685b1 100644 --- a/spec/console_spec.rb +++ b/spec/console_spec.rb @@ -658,4 +658,90 @@ expect(result.environment_identifier).to eq('env_abc') end end + + describe '#webhooks' do + let(:webhooks_service) { console.webhooks } + + describe '#create' do + let(:create_response) do + { + id: 'wh_123', + name: 'Production', + url: 'https://example.com/webhooks', + auth_method: 'bearer_token', + subscribed_events: ['ag.access_pass.issued'], + created_at: '2025-01-01T00:00:00Z', + private_key: 'pk_secret_123' + } + end + + it 'creates a webhook' do + stub_api_request( + :post, + '/v1/console/webhooks', + body: create_response, + request_body: { + name: 'Production', + url: 'https://example.com/webhooks', + subscribed_events: ['ag.access_pass.issued'], + auth_method: 'bearer_token' + } + ) + + webhook = webhooks_service.create( + name: 'Production', + url: 'https://example.com/webhooks', + subscribed_events: ['ag.access_pass.issued'] + ) + + expect(webhook).to be_a(AccessGrid::Webhook) + expect(webhook.id).to eq('wh_123') + expect(webhook.name).to eq('Production') + expect(webhook.private_key).to eq('pk_secret_123') + end + end + + describe '#list' do + let(:list_response) do + { + webhooks: [ + { id: 'wh_1', name: 'Prod', url: 'https://example.com/wh1', auth_method: 'bearer_token' }, + { id: 'wh_2', name: 'Staging', url: 'https://example.com/wh2', auth_method: 'bearer_token' } + ], + pagination: { current_page: 1, total_pages: 1 } + } + end + + it 'lists webhooks' do + stub_api_request( + :get, + '/v1/console/webhooks', + body: list_response, + query: generate_sig_payload(id: :webhooks) + ) + + webhooks = webhooks_service.list + + expect(webhooks).to be_an(Array) + expect(webhooks.length).to eq(2) + expect(webhooks.first).to be_a(AccessGrid::Webhook) + expect(webhooks.first.id).to eq('wh_1') + end + end + + describe '#delete' do + it 'deletes a webhook' do + stub_api_request( + :delete, + '/v1/console/webhooks/wh_123', + status: 204, + body: {}, + query: generate_sig_payload(id: :wh_123) + ) + + result = webhooks_service.delete('wh_123') + expect(result).to eq({}) + end + end + end end diff --git a/spec/request_spec.rb b/spec/request_spec.rb index f88eb45..46efb4c 100644 --- a/spec/request_spec.rb +++ b/spec/request_spec.rb @@ -258,8 +258,13 @@ def build_request(overrides = {}) expect(request.net_http_request).to be_a(Net::HTTP::Patch) end - it 'raises ArgumentError for unsupported HTTP method' do + it 'returns Net::HTTP::Delete for :delete method' do request = build_request(http_method: :delete) + expect(request.net_http_request).to be_a(Net::HTTP::Delete) + end + + it 'raises ArgumentError for unsupported HTTP method' do + request = build_request(http_method: :options) expect { request.net_http_request }.to raise_error(ArgumentError, /Unsupported HTTP method/) end From f525a9a3a207a0e43bc99da7acf768e76e1650b8 Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Thu, 26 Mar 2026 08:46:12 -0700 Subject: [PATCH 2/2] Fix rubocop: extract HTTP_METHODS constant to reduce method length --- lib/accessgrid/request.rb | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/accessgrid/request.rb b/lib/accessgrid/request.rb index dfd6ea3..a890e57 100644 --- a/lib/accessgrid/request.rb +++ b/lib/accessgrid/request.rb @@ -6,6 +6,13 @@ module AccessGrid # Builds and configures HTTP requests for the AccessGrid API. class Request PAYLOAD_SIGNATURE_PARAM = :sig_payload + HTTP_METHODS = { + get: Net::HTTP::Get, + post: Net::HTTP::Post, + put: Net::HTTP::Put, + patch: Net::HTTP::Patch, + delete: Net::HTTP::Delete + }.freeze attr_reader :account_id, :body, :http_method, :params, :payload, :uri @@ -40,17 +47,9 @@ def net_http_request private def generate_net_http_request! - klass = - case http_method - when :get then Net::HTTP::Get - when :post then Net::HTTP::Post - when :put then Net::HTTP::Put - when :patch then Net::HTTP::Patch - when :delete then Net::HTTP::Delete - else - raise ArgumentError, "Unsupported HTTP method: #{http_method}" - end - + klass = HTTP_METHODS.fetch(http_method) do + raise ArgumentError, "Unsupported HTTP method: #{http_method}" + end klass.new(uri.request_uri) end