Create webhooks
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
class Webhook::DeliveryJob < ApplicationJob
|
||||
queue_as :webhooks
|
||||
limits_concurrency to: 1, key: ->(delivery) { delivery.webhook_id }
|
||||
|
||||
def perform(delivery)
|
||||
delivery.deliver
|
||||
end
|
||||
end
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
class Event < ApplicationRecord
|
||||
include Notifiable, Particulars, Promptable
|
||||
include Notifiable, Particulars, Promptable, WebhookDeliverable
|
||||
|
||||
belongs_to :collection
|
||||
belongs_to :creator, class_name: "User"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module Event::WebhookDeliverable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :webhook_deliveries, class_name: "Webhook::Delivery", dependent: :delete_all
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
class Webhook < ApplicationRecord
|
||||
PERMITTED_SCHEMES = %w[ http https ].freeze
|
||||
|
||||
encrypts :signing_secret
|
||||
has_secure_token :signing_secret
|
||||
|
||||
has_many :deliveries, dependent: :delete_all
|
||||
|
||||
validate :validate_url
|
||||
|
||||
def deactivate
|
||||
update_columns active: false
|
||||
end
|
||||
|
||||
private
|
||||
def validate_url
|
||||
uri = URI.parse(url.presence)
|
||||
|
||||
if PERMITTED_SCHEMES.exclude?(uri.scheme)
|
||||
errors.add :url, "must use #{PERMITTED_SCHEMES.to_choice_sentence}"
|
||||
end
|
||||
rescue URI::InvalidURIError
|
||||
errors.add :url, "not a URL"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,118 @@
|
||||
class Webhook::Delivery < ApplicationRecord
|
||||
USER_AGENT = "fizzy/1.0.0 Webhook"
|
||||
ENDPOINT_TIMEOUT = 7.seconds
|
||||
DNS_RESOLUTION_TIMEOUT = 2
|
||||
PRIVATE_IP_RANGES = [
|
||||
# Loopback
|
||||
IPAddr.new("127.0.0.0/8"),
|
||||
IPAddr.new("::1/128"),
|
||||
# IPv4 mapped to IPv6
|
||||
IPAddr.new("::ffff:0:0/96"),
|
||||
# RFC1918 - local network IP addresses
|
||||
IPAddr.new("10.0.0.0/8"),
|
||||
IPAddr.new("172.16.0.0/12"),
|
||||
IPAddr.new("192.168.0.0/16"),
|
||||
# Link-local (DHCP and router stuff)
|
||||
IPAddr.new("169.254.0.0/16"),
|
||||
IPAddr.new("fe80::/10"),
|
||||
# ULA
|
||||
IPAddr.new("fc00::/7")
|
||||
].freeze
|
||||
|
||||
belongs_to :webhook
|
||||
belongs_to :event
|
||||
|
||||
store :request, coder: JSON
|
||||
store :response, coder: JSON
|
||||
|
||||
encrypts :request, :response
|
||||
|
||||
enum :state, %w[ pending in_progress completed errored ].index_by(&:itself), default: :pending
|
||||
|
||||
def deliver_later
|
||||
Webhook::DeliveryJob.perform_later(self)
|
||||
end
|
||||
|
||||
def deliver
|
||||
in_progress!
|
||||
|
||||
self.request[:headers] = headers
|
||||
self.response = perform_request
|
||||
|
||||
completed!
|
||||
save!
|
||||
rescue
|
||||
errored!
|
||||
raise
|
||||
end
|
||||
|
||||
def succeeded?
|
||||
completed? && response[:error].blank? && response[:code].between?(200, 299)
|
||||
end
|
||||
|
||||
private
|
||||
def perform_request
|
||||
if private_uri?
|
||||
{ error: :private_uri }
|
||||
else
|
||||
response = http.request(
|
||||
Net::HTTP::Post.new(uri, headers).tap { |request| request.body = payload }
|
||||
)
|
||||
|
||||
{ code: response.code.to_i, headers: response.to_hash }
|
||||
end
|
||||
rescue Resolv::ResolvTimeout, Resolv::ResolvError, SocketError
|
||||
{ error: :dns_lookup_failed }
|
||||
rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ETIMEDOUT
|
||||
{ error: :connection_timeout }
|
||||
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ECONNRESET
|
||||
{ error: :destination_unreachable }
|
||||
rescue OpenSSL::SSL::SSLError
|
||||
{ error: :failed_tls }
|
||||
end
|
||||
|
||||
def private_uri?
|
||||
ip_addresses = []
|
||||
|
||||
Resolv::DNS.open(timeouts: DNS_RESOLUTION_TIMEOUT) do |dns|
|
||||
dns.each_address(uri.host) do |ip_address|
|
||||
ip_addresses << IPAddr.new(ip_address)
|
||||
end
|
||||
end
|
||||
|
||||
ip_addresses.any? do |ip_address|
|
||||
PRIVATE_IP_RANGES.any? do |private_ip_range|
|
||||
private_ip_range.include?(ip_address)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def uri
|
||||
@uri ||= URI(webhook.url)
|
||||
end
|
||||
|
||||
def http
|
||||
Net::HTTP.new(uri.host, uri.port).tap do |http|
|
||||
http.use_ssl = (uri.scheme == "https")
|
||||
http.open_timeout = ENDPOINT_TIMEOUT
|
||||
http.read_timeout = ENDPOINT_TIMEOUT
|
||||
end
|
||||
end
|
||||
|
||||
def headers
|
||||
{
|
||||
"User-Agent" => USER_AGENT,
|
||||
"Content-Type" => "application/json",
|
||||
"X-Webhook-Signature" => signature,
|
||||
"X-Webhook-Timestamp" => Time.current.utc.iso8601
|
||||
}
|
||||
end
|
||||
|
||||
def signature
|
||||
OpenSSL::HMAC.hexdigest("SHA256", webhook.signing_secret, payload)
|
||||
end
|
||||
|
||||
def payload
|
||||
{ test: :test }.to_json
|
||||
end
|
||||
end
|
||||
@@ -1 +1 @@
|
||||
hTAo532D++IfAePtvgvwjKd6Cx3RRCdoO+77b4yFvgZSGtEXB6mfNGKlEICRL4o0qh0lqimE2/3Sbmnx97lzhfdYXE3b09KewXIub14s1jzTRWzZSRPNcxPeYou10kiN0b/4R/IFON8+/P7Ratok1MHIrAt4M3EUYwtEpzsi3aOYDGlRoLW5rAxFAHRl7zhgkMH3Kzkmjh2tr+QOc5fxqmYT8ETmLJrOtvJtULMW4mfie722zmwm9RFJxtHpz4gnMAwSr2vaZ0edk0sHeErnDJo7Zl1Ul7uCJyXPP4PCqmuNVOQbxjLMNVTmo8ntUWZa7pvocpa5uYy8DIy3iCAYYIxdh/xhJPnqPC0XQnS6Aj4zaOD/fOXSnEuSQjR8d5XyIUAWuywh6Li7qWyj3W8btfmc1HWOo4cwx8LkwRuUG1r/SvMQdIR1E2xtgROaoXZmPOXmYlFJm3mzy+oGOk7Pa/u/1iiBiNEkDx4nhHe+0XuPZcBe4c9PKn3zw/3SOm32SpBqe6zPZI/68VoiXx5wVaogY5gnKQOHNkhi9RhN6SSrM58oS+EKYkCJ4+iRqj80IQNtj4in4pyQ/ONZgc0tjMh1+C342NkhbaJYmGGUPpPVhZPCUf3clbBXqex15+6UkmymWtz3WH6GixPVwrvp1gGUqOmqCiWVNJM/kQ84IOTzlxfeyKjGK5xKhEC3ajYqXDDUDek/tUIhqtOYxBS9T44cKDPDTL1BKFMdIDR7gdtRyQouW3RvD4juqyjh710I9aq3JB1Jvj8UZ7uSHYEJ9NpmllfAx0RrrxMFHcwr3LqjDlDf7mtACfDISVtD6TQLuGySDIhs8gWTiSuZ75jRTswx8y3paI17Yy0en23lDIQ9hlPh6xvvJWQQtCT6B8GCz56GrLOVDfKM/0qeqJtyed2p2/MIJfy+QmqTSMSeQz+wiLi14I2Vgy229A1CW4izMSoR+RqoZzIzCbZg29croJv9DRhp/nVtkjj/enHnHksZKqoxzf0c7nHSWtGSYtCiewDydsBCioKdo9tn/FM9UKpnRAy1XHiq1DvBQ+wuq74KLnC/4nBZ7P90xlI8jQuQfldpDRqKaIYjIvz8UbLdKOjETT1dJjZ9Tk629zdLEVybLSGhoE0bs460oFvlrjJDCXtgi/3tWbTIuwsxY6Iy3eMiS5Kxl42dnlOdVrxwwY/om/MzL4+yvsx38wXS2QG8lDKz2JuGBS4GYq5EEg==--P2KPkoocz8wNhqSN--1u5WuEBtEaqJckCflHkb6g==
|
||||
fYKvBhjXnt2n6LuJUgfMQVDoMcmxB7eSCHv97u4HtXcii1JFEvlFXBMnjz4+4QPcXMlp324db/bfCwSEed+iA9/TTt1HoYYuxypnbiaMnr2HTZVzECLWrlnOvuw9IUIYkSyQ4biW5Tq4VeU1GKpHya5Giop0R3SQKcONMrDhpBcUJ2TepCFYjCp+U+SvsRfUiMnyWk/8e5P+16qSk/s3AnlnlCbX/8kqX3HF0zrXW/tcdVIAvYrMKSzcyixH2YS6G9BCe5cvvL3e3FTnImxscc/cVecXHDDID00yG2brWpgetJamhHpfLmbiPgCpAtz1vtAEDZRJHtXw88AkCJ8wvKIZB+ZMZV3uJaxZkjx1WDDfZUP+Tw43Hq7SrxwGyCmyC8hVAJez1fQh8SoT8tGJcR30UKBxa339K0slXenLsrYc5eVbwSaxEbqgIjJWDdmY9vZ7fhO397WSBaF2tK0aBU36uRy2XMeCmLZX2lnA+gLFSt1D3XMw/sVnEHdYxuFMWg1ScnMlh0nfMMVjpSRXhcd97I/fWnJ/iWcNM+CBMvjCnjVy/6T4ushPO2lfiFAcQAqURDkuKFVpDrQ19V+aWx7uppQrGMvO8GqX+eTR2qoSdk/nnB9f5DsORVx8IBLkCz4p4JZMJiQTOTcp7HhxJf6TEqjCD9E4FwRqZG0VL7o3ZiHrls5yEV4JvNJX1tQZMimttdHP1yUhuR8wtAAQUziNeERq5vuKYhWMOVC8sxnaCksIqfwT6SX6MuwviuwdlREcd/WL4u651dusI1PLdl18n2V4opEJcqr/z4GXHnz2+X+PXTdLGrWn9YdIW0Crm8OVhGHQF/K/zg4FUNPhI6JJm/8Rw4gTdDjniF0C0DPOKZW/7Rrk68g9tn1zB6USDjXcHcxE5rg9YFHXGCnrV2Jzd4JF3M+VLgA4wzYk9IjU6PVqtjWpwRXy09EvbspsLgSYDH4LYb3nCkj/CCYTOnI0l/6ofS63kjyNYG/YD5zKpiRcgU18qiQo1uoIS1QijAt3PMggpIbl9gkcgdbed9qd8vptWNHCFKtNmFo2FKJxsLHg+4OzSotl5PYnYzisFFQyMTKuX541fIMtynD2IgazHmE3p2NdUQKGsZXrXrD35EpDCTiHNQf6Emz30xZ2md7R0NZ4043wezZ6w8hzQSPpVPIrCsrcR5UnfO6KoBeC+iMDUS9+QKim+o7OIjggeE8yDS8lsHASRX05+uUGzz3wId6tvUtgJrXvZbGdFNWCfthvcTeb0m8zGRhHBG3aic3GQ6Hwl5TyXH1lo6v66mJWwPL3YhiJu4kyowLz/2DKiMdvuGAqkqCgokpONzraDum3Mid2N/5B/z0xNnaTpqwWW7eyGc7c/1seNZUgA7Maz7eb1wMv72Xl3VJcxoFYANbCKBcRpFL8ctwAhE3QLojqb4EKDhI31wXynu/i5VVU2kNedaBT8rGhoWvZjrd4v4PNzKy65PJxKkBkQKeeXMJMOmVm0BY4QyCOZW0B/H0z867uB9CjA7Xcz7Wl/5dflawlV1mpr6EQECZd+h8v5mGFeOs0zS87q0xQVfhAcTqYGZKjSIrZZdvppKW1PXc8rNFV/3HbawnPeot4RoUx17bnJmZsdbjYtpq8tAvCH5WRdXsJFt4NvPFdMB9fRmVOZs/XoAtyjie9PJ8cZqjkDk0yRPcB0vOqYLVLuoVm6TrRPtlKTAb4uLj0mM4e--srcdud6UpP4PxVue--MRrzzScZrcPeG9j5CpQt3A==
|
||||
@@ -1 +1 @@
|
||||
qGtDqUHZuqVpajiHSIgxtaKdNgBjWFZaKMWitxGBAtV/q/P0uVAzry+MRonSMglb8SZEW28/LAKF28kUeAMGZVwnbMHAmcmOdOHNwfPBDu/3+c67J0qedEH2k8998o7wBqDi7iFTaWlDQROOi8LiiE3oVnpsRZGXtjXgClYI5ED062ITCd/tMLDsXiZjq/PhodWjI/YgEycwy0r1OldJZjOCzS1VD924AfOIH2/qtszc7oeArEpv8XYm3DJDCixYy5LLnhNIek7o8CHaZIeQxfm9pEh5ZEXaMNfSB4Gq+laAyLoXqsoPWuonjw0273Rjw1U3PU+6GlU85ZUrm9dBnekZiatLIHt3yX1CKynS8yqen0ON+MrP5W98pAa9Njq2GtB2SUrGY1xQtyoIfYPxFbNZoOph4PubcXqCT94bkAbZOiYKSiNLONuhhvKR6r9amh+LlDELp7QLR8EIfRpgoN5D8qhbhEi1/c2Sxogv/7+YtkpaYq47Ram2THll+URQ3L4iLKyPIaimxt9vsqdO5COn69fTqtqkYR/fKy93/3JIJxiNQ8I6z5xhGRLFk9ADWB3ZVz1VzXzW0Q1B0UQmMg0KEEM9ZQTxignWeJasbG2L/T656Lg/wPdKdAw0jcZAOh+BGtePgOOjZ5Le8rweSHKou0kTn1EjR81csRuSnHEEGTdlNp34LC0NirqwEwmPySRrQxmNwslS82R8uQvZQ+XtalJBBvUVwaB+o2rm9AfZFzzjGmZhVOKCQLZx4ugG+CUfc+soLGuQtcyPQe+0V2hJ97zzqdWxKvdUQ/aCerBwUQczl7r/PPxKucl4LtF+3mNYNMWjIdNIHJRzhvCnmZ8Zj1TuO4SGWMKNx7kwNP54dd4pqRD6Z0vm/x8zZXeccfu8Y39IOlawyEDR7idd0CSZ--41mPjeBDVAaVR2Q8--Qfq5LzxnQOYGDaGie2K6FQ==
|
||||
DaJ7nO7vhM/zZdRzh0AeG3bZBvfMp38E70+jOoUG/oLykxrem/qoKauYuTuQXadi0EzN09SYeWmPeA/p05PcNNOsw1IvpoQnrUqt3Jl34BKpW+3dcuVAafEiFR5Cvg8XOQu6Wu7KBklw8ko+TffCFkF/dqQ+60/snEGGqC3PqavHfmfbvwJyiXHt8/R5kfraUUd4hm3Rf1TikNVpnWTFOG3eMO4tcml4bmjgJAu4wGtAyLfosG/OaqQba4QRcXsF7HnH5yYcb04aEdFSmbSFIvm2OuGvc38KTXAsaFCZnvW3kIa/mMCyh5AGaE4srT9bmF/wti8CzqjpRDHRAEf4rqfYYTV+dRwBn1115rbEdQx1GDAwiuU/g9jqf9VhiPfaNRmcItnZflejWfi3LiPNFTcN7fSB3r1h4gg13dUb5C8wCRFc31cWNpERbzxRaneVP2iTQNDIuFcraf6aiEVAzWvGwv6SyLpuuWLL1sin3O2IrdZQRnO/VzaKnTxtqMVAFq+rxIA/511fK2+Gh970JUMXaJQZduiTv9aBar2q/raq7pvalrBqAWI9NgL+i9sO7Tf2stgti3W5FIsMUDQu54DzMpq5+mL6iEI37Peb4/jsFwDPicsM1mLRtXl3PiwZeXmURLUur2XdKaZB7SEhTYOhshIDKLR3Szn4d8INoZpN1RdHI+RikLISqMPlV09T32+I8sNRZ2V6+0ddqidtpyjUtsFrLNgo2gVBCepGBjrV16QlIpaJJBLzIZeq71GHJ8mc5zyYeJR95vgAecVEnMVmjNk1H1cL8C0lQFIlt+OsRAkokq5sF32OB/2YyDTFOHUVCvTB4P7VRHO45nU30hkCVGNha0yo5iyuCh4fFAjJbKh28BWyJ53F8kMNwwlsau/KSF2hJrcn4eGL7r2CvI6R/kLOJkVryEHHUb8xmc8HJhLKoscrCk26v0/LnYHH0Vui+3wmFsC9uATgVqURsuvzvzQkaX+qClec+F5YVJ52MUWF/b7BGCusWnQ9A5W2Jb/bdxHqKgPG4LaC8sdDwbgOCUWCTaOrEAuCfQUK+Nv7aH72BmdTLkMliOSnJWOZLz/Uubzagdy9sAuuqctFg3M3QTPi9Hgua2fch7XgGv+P+P8WF2AppwDIuEhKpzDPiJvtvKI1doOTtw==--LL+6EQr1zbYQIsEa--B0if+ehjZyS9BYxbPIbYKQ==
|
||||
@@ -1 +1 @@
|
||||
gJofgOA5kUBwjcFwc6IDRjLbYQfG7qGpm8BcAEnS0FODuMbEE48FKN4jdUfhGAw0yCHu9DT1cJiP2wKR8s7nyf6dUGl6fPljR0sKTvO0eznmWigWCc+Ki6zVef0mRqQ2lZguiokjapG9c4qBcy2MYhVOhw3JAYvH6X4piiyO52nLL/kKLEQ64/uqC9VDKgR8TRhyVRqQN2jvB62VnIGxWX/qSPcapF/jw8ECgPsXgOSY/s4oNvQ8gCuZMweDlSYqFLAXpn2GP+wAaT9u6UmprPx7niDb1EIIm+LFWU1Ar4lY2n5Mo9ckN0ddsoMnwlsYTjlAWnIn9m0o/GbaxWmtSScnxHXbzqr630rEffvZrzLTG60dxkI0it8cVXwSHI6pMQHZw/lUkHd13IHMzR9eNGUq4pV+fdQjhvcfS2sTwVeCZ6VV1DbnY1vOeyMnVfvFLlJ7eqChkpFBAmOKS4hV/Mrqks6OP7K6ubyXt/REwuNhRMeJ1SJAfgiDSKbmNFtSnODyXKP1nFTCvvdndiP1H6t8qkJo8COSnSWWzcyyVKMwEKwbOxxtX2C+V4aySTllw0gFODLjQPxPBwqnkwcEcDEHxtsgImOYnOc0Ro/4faI1C5WdCuF6+VOBGpq1nI4HCQqepx4b/HZt/EyJSaQU08emhHht2vlCqJUN2yPUxnbCKBAbQhz0khRANBWIf8q0wfwSLaAqQHJ9FsZp4ztOC5U4lULzaMJh7dBUbv/9+mH6Yb3lFbg57178GYCgbzFwhvEOlEKBjfwDRFNmYfiVH4YYbZTIgQvu3tSywNNQJ4flu6TSjMC6zdOBi41BAIXH7j04zwpQV7Zndlu3xE1LTMbcLfBdqzOBGuRVD5KvQH5XFj0RfKTg2DN7Mo2XwSb7IZnl3Ga/Tuvb7DwKpRtql2RCYIYXwg5EnCYF3rw8J9m4tLzr9AFh3KALq+Lnfa0d1MysB1NQJ5XqtJ3jeYW+sxv4ivv9gpzQ9SZF26Z3rZoQHnb2S7usIgzEquiDyhAQr67AYOMJe5DKRw/QShJwYK+k+n/IrpQxUBnTrZvf2I47KF6UkhgWpZgmQaDB7WR1PePHVqJdkdQgqQzUU2t0SQIUY0FHH6MRLyHDcqOFiPl76HNFOmElZzrfXzqtwQdXOau7hV645LqaUy2xinA97zkEdpAGr/P/eloveh6wIgSIZNTsm8RbgRkl8SS1O9Q9goxMir4S3JvjWTJmAhxKaKlGeIVcMRVIv5C634Ugpp5jzh8PHX9nHAN2w9HHI+8AMaBUeFyuixbfk88EIvby72wY8MWxYzv1H/THWSL8WTiQJGxWE4PjSBQtwuc1zzHqG8Ve45Z1nIkt4TmC1VXftUHj5SghncAHCA27v+Hdk+cjSw/ymaVwIRreaZYMjOs2xX8XGOYXpbChFJaMk7ywR3FyBzOVvFXGnGCuA55f+j/7E2KuBI4lnr/mmCTeYvLOBrBDd2c1sEjKTURM4cSlDBw9NKc8oloxYZQKn9vA--z6fcV0Ojv6v39SCA--yOvnjjoQdp6k7P89GMEgYw==
|
||||
mD7x3X/gvld0L+2qVY7VrjA10FcuFGSXkYW6Kw2gvx2MP9rwmTZduAnTrE4X6q8CQKVJA7kZhHj2Kkng1ukrJzDrSionpAp+YEQ+7dzpbO/nBfjyLDnPN17rOayrN7N79mQVwGHI57XAGT+mVUyZetxzaZMroPwPpt+1vtJnmLbTikMK7qqPfwJL8sqzoVPG2GE9qwB7x4Zo9wGtE5TyCHYUfNmz8wNlfgDKxtqajJsm8wTb+8/OSd5wVwCdulGtEYh6TGvrOgjCK9qn22hhtl8NZcAbhTBKr2tunkXirYRkPw9XDwvIGDAVL4ZwUZ7A7cRMGJ8Wyb/C7LHfTo49+Lj1EdRdBcyyz+DCpmXt7115JBClNS4vBeQ/t9KL9SepySeG38dsHfjJtZnu4bTP4IeT7S1Yqu/3e46Tqg6/2q8nPrS+FcSvLUy3baKi0RrgvBdE9JR5GCDmS3LCIu64f04cEzhFh735iyZrrsfyBVhOEh0hZIGc9xKvByDnnIH0pz/c+8l5LlyW8gqzRb7y1pg9W8qF1QK8kb7/UyiHovVWoXYwKZPNVy9qaDYUYqcXSpnopV85iTPWR49XJO2Q/TkX0vEgzOe9/REpRSbcvFCPkvh4rCScuqMlttVvnJF6JBHSbuXAkJL+yWZeqzc5LYQ8Os4i1VTPM2MIavw5ldU8I8knNY2wUz3dVbz9accJwf1v7fgSU3tBBRqsbuvpGIiUrJtnvCNWvtUYXBHgEcEabnAXTHO7Mg6pwdI9hSLlz9NeBefqmBb6xnv6bfx9C8P2dTdHCSaDWhh95fd/5hJBMCeGhnuZ7O2NrMYP0uUJfzdDj5JSY2p2+Tvw7iUeljh/Tua1g7yhLICTKkuu2g3G4vMcO7jcF9j5ibCbHgcGTUm9oghPbAINfg8JpdD0tA8aTQdLUdq5ue91PL/TUi3GgwzfCwUfxZFz1HjudVZyTQuIfuVWnap5r9CcEW4QjTDOreYNCqYd58XYj/Pm9LPiK9LcfYarbmPobUiUg63IW5jXfttCaAWToDWc3YuNU8NLTd3axXkF4NkS/GBRk18NqMQjbwXYR8eQctbQIEhnqANTkUwVp6qO3M//6nBTf3dp/dS18EmFgyGRm6cGYAsOyBNXyM1UCX6h9uG3scZPxACffEViZ0dNVJkOySVFFMEnSpY04CmVB7RtbAnBxleP//2bvJucffRVMFm8y42JzxCTC1qao2Es/IIj9epSK3l+YIGcW27QTr4x0I0swG5cOLxAQ2/HUdwZmCk1Oqjrtn4prHC/gl5+LIzsVmQ2dfcbZ5dyM/6IRyJNMQlknLv9FerCtURIzb/O0Lx9EfrQH18KP9KFPhgCMLsa0kt3JdYFfoAhFBrsB0Ioc4+F279F1j5+X5M+zCXkYq/71BndgCALwC7oVCXWFnl95jYA6yDTHMIr8fRjqV744wU9ZIIZPlaf2+duox9REOQBSnj+olkIHfh1AYHzFs7Sa9z3d2jBoplwzVqXbPjd4u882BinLjPy3UYPPnXtx7ND4MgsP5/NNY29OXUChr5OL658ayJADTdouXgIOghmfEKOacIn3zCiJbSJFH29jgS+MGIA5l6OxHkzj787MR7vC3mjF6Ut0f97LYVEMUshh92DhHao/oM4lcvdoQmhO5OI0zfhvA5vXhTvC2BI+CQjgRKrjLkCpEU8ZvHQ8ceO5hzpx/vf9v5tJVX8Q1EOZ7QklD4p4I9OVRWX3g7Sz4iUP5edwG3iVF7hAw==--mvpDxs3LkB232zsJ--tC39AAOxnHH12G7pzQ0nWw==
|
||||
@@ -1 +1 @@
|
||||
DHJSW4c/xFkMWWYW3b1uqnoiwuqVvRabPVvSKZWaoM+EPzUses5EaJ7GBXjpVS/4TYQ+0XsuLBTniuhUtFx56PHo1vWWFRy3n8I1YGPOs2X9mvG2wK5MlwSGO55xatQ1MabRyfjVJfW/inx4oWaqh4IsZhQ5uL/PUWwWy+7tKx3yWbPDa9K2vsgJuJJqZav0gJ7Bn5j8sdpj8xRofA0ZmiA3QxVIHOLiBYRXH9gmuzJ9BcDiAfZ1EA2FnM7T/2v5u6K4k5Lkp4Khn7t5kSF5hrUIuU7dWFaz4WFS6sfcCkRTHgPKu1ZfunzQl9vDXxkwVUjnfGnIk6XUQz8qfI1sDwhxjaiKAtn/6V8/zSo0v9lj91kfVWg/e8byzDxtZhRN71lnDgwaRIQxSDumfFIX2Yd5aXoQw7b+VymZaA+8J7vh18GPkBSsWTZ9b1fA1iVHncSDSEYH/FVXK3Ul/mpcN1bJrlBXMkZ0HmDA1EXhMJqPGosE6/OT5f1Zsn5SSpbqM6VZjQ6LOHwNCZs24F8BhGnCQtdl/VlIHKyGq8/zcyWUuROAVSxV9fiMDddWrprjDsjXPUigFqgtyco1CCZ9p6ScMihOzpOhT0dcvvjKZY56e3GR6n7u5JfSL9CpykQOXATHBXJlAc3HYxPxfESkEa31VmwZGOZxUsTpIZDLC/7VCHXGgtvOYLfoPBJBZ7BP8Eun+q/hz4iAr40xlfSeg3G18JpQZYHeF2/3WucrTW8NinwkPW/Yr/jtd9duyuIZNY2/zGyLiyoKiuHnk/iVDsyLIth1P7ZoQNyyiITzB/OQ2KqAvqwjnNYqLfBAQfe6grBdAGrNInsBHc7HNMcuHP9bvvS2cjlepP4zZyKo5oiAkmaWUPuBkUqmY6ZcOmwLTwl//fBXlUzCuUhR+0k7af6IlCsDhmp0HL575JmYN+HKGZ2LtQo+1v4MFJR5c2gh0Q4zTAkJsaOz+39aI5e2VPXkpXNyz+VVFJO36lg9tpaomUgtI2BjrO3Y3qQ8Y5jilnSX2HslL4lFMLDvyphDYJx7ODPimUjNkJ616YXSgFJSBUVa/ui1CnxNqOfO+AtuxJz26cuQw8ORKZNihd0QT276pHGhnXb+B0JT+XCuniZnSF+oCn6Kmbt1bsElCN/sMrII7UdsRGD5sQWicjpOM0g50GPYF9z6v8tCYlEmTQSjp7/KOwKLo4l4re3KeuD4FuK/1n0x2CNm1MLbvXcOGkmCa3P79YsKl8L1x8+hbNoHpEpswc0dPb6FLthCxCqrGQiREO4pbTrLasVL6CJA0eRd39B/8AcXnsJwNj4B3a2PbafnkdhJMPAuXvieFFN6leMXBF0YCsg+1Br8h9YmZfzmkECo9JVkNCfYdNbkfo8AA+hHu/youi6vcJD+x+eKTVdi88jy9qwyDdzIf8GdiIHy00PnfXj4lgTx7SQr+WUpsUYnQZh8xqkAuh5Lgi7ExqZMjjo4FORFxoJHToNU6c44+aVb36P9Fg6v3YedINYZkRWMTUq/f9O1zoOx3wIE10Qk4SkF+MLPSuLJWbEmfCyNZZwFUH7xde9lSDfB1s+1XhtEz0Li3Kq8jCeftRcIZvQ3LVjFuirf6nfZmZoTjaVDyUiWHtXyrQ==--NSFlvZtAfCQmzAp3--o8XatBC+Yh/p55fY485A7g==
|
||||
hIHE2AaAwWrP9YzliIwVuq7iSyy81LNMmeScQYahZqbNW5pgdnprKwMVwp/R15mZrI/tNZI1utxC1FFX4BTsRgkYhVGrQDx74bB3EaT4JfEpPARpqMLYWMWqQ/xzuvhknoweR17Bty/E9gMgdw1Ip7pmQpZJRQVntSZIMxr+IysR7cbXm5Em9+LAxhm4ku7Zu5sviW/ljzOH/vJ7WvZzg7E7SpHNYV1cALbIjKvQKcBppR4tR0cXcUZ+7K9f5Qxp84XVWi5CA0IxLZ07Wq5QAPXN5/Cc8PkNZB9y7aRQ6lfaB+FzSHpE51I4u+zp9j5qDWU8+vAj5vskbDWn6t2KLWHPJRydhSo9u4Irh/VuzctL5JpSSfOJqmA/oCFnEEsrafcL+VMTUmTODLAnhMB31zOJvny57WN5oClFp/TdEoyvJy0bKkkMPHItNVoESKgyzdAmK57UyL19kyyuaJUw+qnL5CCOzSKDq6jd8ImH7AEGVOqrDEsVNQvBCPEh0qlxu1XY2FdsNdgSM86w6aLafX3DcWuawLg1CjzZDcc4GKCp36PjNqeTjy82biQ85kF5yXbLv/KjY4gO363jV9tnu15tQOc9BLaR12lbIerdUTvPQ2j1n/TReWIbmyUyZA/yoH1R0lz4XIl5yFe+jZ6AyyJ4SN4V/XNe8N3ejqKghuTuujfn/Eo5GxzMmGrgUn4GNtY1Z1p8vjAVb3Ime5gDDp1pvOqyPUlqotjRpn0V3aYbWoa1y85j+geQimNyAZT0lkbd4f4ExdsNgVmgd4iPCLiUuAfdH5b0w7IC4c8KgPfox91bKOQa86GytG7ch1h5+vyL/1fk4oXn/fj1vcszF+xzvYxmbZhh9DoYrxbZSzfjCZNK2I5DLeDZioWTO/HVVoUx1Gpp7hKR4iJxa7793m9BfsNz+2agdi8N62WNiXGM8hW1jPVKNnAD6PyMx4NlSe6vwyVBQA3ziBu0PxT7NCH43zqG2VpuQyV3GAmcSqGFDvbiqub9eIQOvBTylSE5KPEBTIdUF/JIe/mysKaMuaph3gOp8YT91xQ4DaT3DWYyjuRXhItWi5nPUbF5WGSXHPAAhXD/Qp0YNiDzClfIjinpqUF51FhfACCuMdwmKAf9almMdxcXKHvwHLpFJbepHMeuPMlbojNEvfwLUJi2IQ3YFQy5qLbu84aPTW9oSYZEJ0Rx2gOSB1wv3FNtcSAjDiq+plQQs4O1MxtmlcwOeHhwXGSVnnEbjabXJ92JI4yhReIDtEzXrSIaNu4HZtDdzQwe6KwyXoR2dxxcrhUXe2ImjxGLlhnaygdvr5DVQx8XfcOFCIHcSlxCXND+jg9EdBS9m9wW7PpG60+40BB1hjRkoTrzdW9R73m1E22CKOVlTzB8Am9rk1fmmxDyA8/SsErDo9J8LSmzCyQVomjgPfVtAXjcaME53UKL/Q92DLlJZv7koj7bSpN7XSH+ri50BrMCIURJuoivBgu6c8g51OcDMJCUYVajbws9W8hiA4t7dhRsC+1jh+0T/qfu4Kelo/RC5rLMc6de+Mmw0E3HIpTEBttJJJyiIpT0UDzoGcDpq+4Fa+zPRbxIPcD1NnfuhR/og6KsWvLeIsUfKDD+TI/gSE2zxYfC+/XaZO+8TRNc99Ep3qNJ5ZIy1hitn/wMUvCpuMUZAxjOlfh6CZekYgOFESkGKL+b0/O8xeqV7x8qE6SW7pTjCOoykD/jB4XNXY/M2RA4sEIgge0W8v4ENzafDsIFt1eQI/T8vGnbcUxvt7TaJdQ6hwanDKZ8kdmBrO2QG2+Y2vVhoxNYm06gjNAzygnjpYXyUtHLGGhAUjqITMpdXL/ZUnSzN/U8ib0s/p41Rlej472d3NXkbyKFW3E=--bQMkphrAKssxoUy8--J00s66sXG0GcHRoPPKmG7w==
|
||||
@@ -1 +1 @@
|
||||
UFzTQdDbGY5BJ/SHVPWzNlBZoO2+BF0eHHwGICuSB5pi8ESFOHrGhmT7a9qpKLl8cbBMTPPayqLEuuuRbbtXEBS3EiDgAP7XaDViQHHAzBwgaATFT7U+1svnAwppTrAToXJlk51BdPLVRzftY9VZJ9RoDTfzKo0WriuYZ8KIRHmJjaiNDJK4xnqKaqPpF6SU0OVW2Qpo6xAKJVYKlV6SEO2Nt6C2lYra02G49f7poBypJnqouDnn+zlrLd2VGfDXOqFdi+R8T+ZS4HIzHUPi2IpEyg9jFt7qCNpSX7gy2NPZEx2mQmLC0jf5E+ew5ytKqxX1HnoJ7u8IMFvM9gQuNyA1+gaWT42uAqhUB9JKMoyE25KOSnV548cOfxCLnRNb0TgIbp5hpl66yjG32bQkyrvkCRmLBtkii/aGiwpA63UP2DTy0C/Nn86PFXJslHnVxbm8iSCRbhlu91YbmG08pzyCZQe9nRhIX2QBPufIELaJiMcFKYGfQ4Hs30TVCQ3ATdnC1UfWa7BdUsMD92ckneSrexaNofrH4WaRxyIf4VMtfRXqVgv9ovv5I4sr5BwYrPN5WvS3ksvAaoOJ/EXeq+io5OMB2ItzPJucfw==--q/sQOAAr2yruQ50H--dPendfhTyJuxPPImcH2Vqg==
|
||||
GmilAzR45j/MIl29ZP7M7NkyjMrAKvQ3s4He/enEUhetB3IHVS+EotbU1RW5Zvaeq8a0NFHW8BFdF+DQ0MU25s5Tn8aDWe+GG/7HlN1O7r00Wf8bOgyCRPJAUmkkP1gMwLtS4VCFseMXancAKyOhh7MXEs9GLoj7IxczxOwo6wAmWvQQMiFewo1nWzXQIa6kEe7MGixJpmJl+FYdDmHHxNZ74le6qW3zTl5lLhCi5aC4ntfioOQPBX5psJ4yPixSaIAPucjV9pjaPaXORONtBsTp3CUA8FeLh+/lxA26JNqHR5PP0BA+KyGjJNGlK/yS33F7UsExyUBD58UADbaoeBsarXSyLP3OWhT40UgPZvkvQP7MoVsJurxjYCVIzYLMcwAqWFV1NakWp/aJqAENopD0XT0ODh1eslUN7kYYMvYg03IHevOkvz3UPhx2L/s/tyqwa3D8YGzVFxEq9Y6aMDelhJ1/T/0+8FpTMnvGQt+rXfCTzPr2AeQDO1OnLFtiksFL8AqDuKKXh6OspY2z+TgSpCR+6uqIaauh4attlmoWlG5bvwKPuCdJ4hxdaYT+bLcoqN736j7db2RGF8xNauiCxYeoqZtg8MN24geNosBnIqEIQmrejZ/UCZtFWVDhtnw/xYrjDu6/3PAxTWfTqNc/WCXrMSrO9E3upAEQKSiveWaPWFdf/opW75IQP1FSLnZ2IAHm4HH1zvvdO5aL4wwB76sGxq7UrNZIBrCsKSAemIJKimuUoZ5mdE/9ZyzeK00dDvuQ/NUulQLEfa1PcQOjvd7YErZJsiaP4BF8/d5pBv3y68rpxsHp1bl2hMkqS/ww+DpYhB37OiSpdxDSR8e7MOE=--L0z/iKlmgeOYpAtv--SJvijuKNOO/M1jGTEdzxFg==
|
||||
@@ -67,4 +67,7 @@ Rails.application.configure do
|
||||
|
||||
# Load test helpers
|
||||
config.autoload_paths += %w[ test/test_helpers ]
|
||||
|
||||
# Auto-encrypt test fixture attributes
|
||||
config.active_record.encryption.encrypt_fixtures = true
|
||||
end
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ default: &default
|
||||
- polling_interval: 1
|
||||
batch_size: 500
|
||||
workers:
|
||||
- queues: [ "default", "solid_queue_recurring", "backend" ]
|
||||
- queues: [ "default", "solid_queue_recurring", "backend", "webhooks" ]
|
||||
threads: 3
|
||||
processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
|
||||
polling_interval: 0.1
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
class CreateWebhooks < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :webhooks do |t|
|
||||
t.boolean :active, default: true, null: false
|
||||
t.string :name
|
||||
t.text :url, null: false
|
||||
t.string :signing_secret, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
class CreateWebhookDeliveries < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :webhook_deliveries do |t|
|
||||
t.belongs_to :webhook, null: false, foreign_key: true
|
||||
t.belongs_to :event, null: false, foreign_key: true
|
||||
t.string :state, null: false
|
||||
t.text :request
|
||||
t.text :response
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+23
-1
@@ -419,7 +419,6 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_15_170056) do
|
||||
t.datetime "created_at", null: false
|
||||
t.string "title"
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["title"], name: "index_tags_on_account_id_and_title", unique: true
|
||||
end
|
||||
|
||||
create_table "user_settings", force: :cascade do |t|
|
||||
@@ -456,6 +455,27 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_15_170056) do
|
||||
t.index ["user_id"], name: "index_watches_on_user_id"
|
||||
end
|
||||
|
||||
create_table "webhook_deliveries", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.integer "event_id", null: false
|
||||
t.text "request"
|
||||
t.text "response"
|
||||
t.string "state", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "webhook_id", null: false
|
||||
t.index ["event_id"], name: "index_webhook_deliveries_on_event_id"
|
||||
t.index ["webhook_id"], name: "index_webhook_deliveries_on_webhook_id"
|
||||
end
|
||||
|
||||
create_table "webhooks", force: :cascade do |t|
|
||||
t.boolean "active", default: true, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.string "name"
|
||||
t.string "signing_secret", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.text "url", null: false
|
||||
end
|
||||
|
||||
create_table "workflow_stages", force: :cascade do |t|
|
||||
t.string "color"
|
||||
t.datetime "created_at", null: false
|
||||
@@ -501,6 +521,8 @@ ActiveRecord::Schema[8.1].define(version: 2025_09_15_170056) do
|
||||
add_foreign_key "user_settings", "users"
|
||||
add_foreign_key "watches", "cards"
|
||||
add_foreign_key "watches", "users"
|
||||
add_foreign_key "webhook_deliveries", "events"
|
||||
add_foreign_key "webhook_deliveries", "webhooks"
|
||||
add_foreign_key "workflow_stages", "workflows"
|
||||
|
||||
# Virtual tables defined in this database.
|
||||
|
||||
+129
-18
@@ -1248,7 +1248,7 @@ columns:
|
||||
- *9
|
||||
- *18
|
||||
users:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
- &37 !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: active
|
||||
cast_type: *32
|
||||
@@ -1318,6 +1318,95 @@ columns:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
webhook_deliveries:
|
||||
- *5
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: event_id
|
||||
cast_type: *3
|
||||
sql_type_metadata: *4
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *6
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: request
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: response
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: state
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: webhook_id
|
||||
cast_type: *3
|
||||
sql_type_metadata: *4
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
webhooks:
|
||||
- *37
|
||||
- *5
|
||||
- *6
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: name
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': true
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: signing_secret
|
||||
cast_type: *7
|
||||
sql_type_metadata: *8
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
- *9
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
name: url
|
||||
cast_type: *15
|
||||
sql_type_metadata: *16
|
||||
'null': false
|
||||
default:
|
||||
default_function:
|
||||
collation:
|
||||
comment:
|
||||
workflow_stages:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::SQLite3::Column
|
||||
auto_increment:
|
||||
@@ -1397,6 +1486,8 @@ primary_keys:
|
||||
user_settings: id
|
||||
users: id
|
||||
watches: id
|
||||
webhook_deliveries: id
|
||||
webhooks: id
|
||||
workflow_stages: id
|
||||
workflows: id
|
||||
data_sources:
|
||||
@@ -1448,6 +1539,8 @@ data_sources:
|
||||
user_settings: true
|
||||
users: true
|
||||
watches: true
|
||||
webhook_deliveries: true
|
||||
webhooks: true
|
||||
workflow_stages: true
|
||||
workflows: true
|
||||
indexes:
|
||||
@@ -2858,23 +2951,7 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: tags
|
||||
name: index_tags_on_account_id_and_title
|
||||
unique: true
|
||||
columns:
|
||||
- title
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
tags: []
|
||||
user_settings:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: user_settings
|
||||
@@ -2991,6 +3068,40 @@ indexes:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
webhook_deliveries:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: webhook_deliveries
|
||||
name: index_webhook_deliveries_on_event_id
|
||||
unique: false
|
||||
columns:
|
||||
- event_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: webhook_deliveries
|
||||
name: index_webhook_deliveries_on_webhook_id
|
||||
unique: false
|
||||
columns:
|
||||
- webhook_id
|
||||
lengths: {}
|
||||
orders: {}
|
||||
opclasses: {}
|
||||
where:
|
||||
type:
|
||||
using:
|
||||
include:
|
||||
nulls_not_distinct:
|
||||
comment:
|
||||
valid: true
|
||||
webhooks: []
|
||||
workflow_stages:
|
||||
- !ruby/object:ActiveRecord::ConnectionAdapters::IndexDefinition
|
||||
table: workflow_stages
|
||||
|
||||
Vendored
+41
@@ -0,0 +1,41 @@
|
||||
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||
|
||||
successfully_completed:
|
||||
webhook: active
|
||||
event: logo_published
|
||||
state: completed
|
||||
request: '{"headers": {}}'
|
||||
response: '{"code": 200, "headers": {}}'
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
successfully_completed:
|
||||
webhook: active
|
||||
event: logo_assignment_jz
|
||||
state: completed
|
||||
request: '{"headers": {}}'
|
||||
response: '{"code": 200, "headers": {}}'
|
||||
created_at: <%= 1.week.ago + 1.hour %>
|
||||
|
||||
errored:
|
||||
webhook: active
|
||||
event: layout_published
|
||||
state: errored
|
||||
request: '{"headers": {}}'
|
||||
response: '{"error": "destination_unreachable"}'
|
||||
created_at: <%= 1.week.ago %>
|
||||
|
||||
pending:
|
||||
webhook: active
|
||||
event: shipping_closed
|
||||
state: pending
|
||||
request: null
|
||||
response: null
|
||||
created_at: <%= 2.day.ago %>
|
||||
|
||||
in_progress:
|
||||
webhook: active
|
||||
event: logo_assignment_km
|
||||
state: in_progress
|
||||
request: null
|
||||
response: null
|
||||
created_at: <%= 1.day.ago %>
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
active:
|
||||
active: true
|
||||
name: Production API
|
||||
url: https://api.example.com/webhooks
|
||||
signing_secret: p94Bx2HjempCdYB4DTyZkY1b
|
||||
|
||||
inactive:
|
||||
active: false
|
||||
name: Test Webhook
|
||||
url: https://test.example.com/webhooks
|
||||
signing_secret: H8ms8ADcV92v2x17hnLEiL5m
|
||||
@@ -0,0 +1,108 @@
|
||||
require "test_helper"
|
||||
|
||||
class Webhook::DeliveryTest < ActiveSupport::TestCase
|
||||
test "create" do
|
||||
webhook = webhooks(:active)
|
||||
event = events(:layout_commented)
|
||||
delivery = Webhook::Delivery.create!(webhook: webhook, event: event)
|
||||
|
||||
assert_equal "pending", delivery.state
|
||||
end
|
||||
|
||||
test "succeeded" do
|
||||
webhook = webhooks(:active)
|
||||
event = events(:layout_commented)
|
||||
delivery = Webhook::Delivery.new(
|
||||
webhook: webhook,
|
||||
event: event,
|
||||
response: { code: 200 },
|
||||
state: :completed
|
||||
)
|
||||
assert delivery.succeeded?
|
||||
|
||||
delivery.response[:code] = 422
|
||||
assert_not delivery.succeeded?, "resonse must have a 2XX status"
|
||||
|
||||
delivery.response[:code] = 200
|
||||
delivery.state = :pending
|
||||
assert_not delivery.succeeded?, "state must be completed"
|
||||
|
||||
delivery.state = :in_progress
|
||||
assert_not delivery.succeeded?, "state must be completed"
|
||||
|
||||
delivery.state = :errored
|
||||
assert_not delivery.succeeded?, "state must be completed"
|
||||
|
||||
delivery.state = :completed
|
||||
delivery.response[:error] = :destination_unreachable
|
||||
|
||||
assert_not delivery.succeeded?, "the response can't have an error"
|
||||
end
|
||||
|
||||
test "deliver_later" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
|
||||
assert_enqueued_with job: Webhook::DeliveryJob, args: [ delivery ] do
|
||||
delivery.deliver_later
|
||||
end
|
||||
end
|
||||
|
||||
test "deliver" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
|
||||
stub_request(:post, delivery.webhook.url)
|
||||
.to_return(status: 200, headers: { "content-type" => "application/json", "x-test" => "foo" })
|
||||
|
||||
assert_equal "pending", delivery.state
|
||||
|
||||
delivery.deliver
|
||||
|
||||
assert delivery.persisted?
|
||||
assert_equal "completed", delivery.state
|
||||
assert delivery.request[:headers].present?
|
||||
assert_equal [ "foo" ], delivery.response[:headers]["x-test"]
|
||||
assert_equal 200, delivery.response[:code]
|
||||
assert delivery.response[:error].blank?
|
||||
end
|
||||
|
||||
test "deliver when the network timeouts" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
stub_request(:post, delivery.webhook.url).to_timeout
|
||||
|
||||
delivery.deliver
|
||||
|
||||
assert_equal "completed", delivery.state
|
||||
assert_equal "connection_timeout", delivery.response[:error]
|
||||
end
|
||||
|
||||
test "deliver when the connection is refused" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
stub_request(:post, delivery.webhook.url).to_raise(Errno::ECONNREFUSED)
|
||||
|
||||
delivery.deliver
|
||||
|
||||
assert_equal "completed", delivery.state
|
||||
assert_equal "destination_unreachable", delivery.response[:error]
|
||||
end
|
||||
|
||||
test "deliver when an SSL error occurs" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
stub_request(:post, delivery.webhook.url).to_raise(OpenSSL::SSL::SSLError)
|
||||
|
||||
delivery.deliver
|
||||
|
||||
assert_equal "completed", delivery.state
|
||||
assert_equal "failed_tls", delivery.response[:error]
|
||||
end
|
||||
|
||||
test "deliver when an unexpected error occurs" do
|
||||
delivery = webhook_deliveries(:pending)
|
||||
stub_request(:post, delivery.webhook.url).to_raise(StandardError, "Unexpected error")
|
||||
|
||||
assert_raises(StandardError) do
|
||||
delivery.deliver
|
||||
end
|
||||
|
||||
assert_equal "errored", delivery.state
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
require "test_helper"
|
||||
|
||||
class WebhookTest < ActiveSupport::TestCase
|
||||
test "create" do
|
||||
webhook = Webhook.create! name: "Test", url: "https://example.com/webhook"
|
||||
assert webhook.persisted?
|
||||
assert webhook.active?
|
||||
assert webhook.signing_secret.present?
|
||||
end
|
||||
|
||||
test "validates the url" do
|
||||
webhook = Webhook.new name: "Test"
|
||||
assert_not webhook.valid?
|
||||
assert_includes webhook.errors[:url], "not a URL"
|
||||
|
||||
webhook = Webhook.new name: "Test", url: "not a url"
|
||||
assert_not webhook.valid?
|
||||
assert_includes webhook.errors[:url], "not a URL"
|
||||
|
||||
webhook = Webhook.new name: "NOTHING", url: "example.com/webhook"
|
||||
assert_not webhook.valid?
|
||||
assert_includes webhook.errors[:url], "must use http or https"
|
||||
|
||||
webhook = Webhook.new name: "BLANK", url: "//example.com/webhook"
|
||||
assert_not webhook.valid?
|
||||
assert_includes webhook.errors[:url], "must use http or https"
|
||||
|
||||
webhook = Webhook.new name: "GOPHER", url: "gopher://example.com/webhook"
|
||||
assert_not webhook.valid?
|
||||
assert_includes webhook.errors[:url], "must use http or https"
|
||||
|
||||
webhook = Webhook.new name: "HTTP", url: "http://example.com/webhook"
|
||||
assert webhook.valid?
|
||||
|
||||
webhook = Webhook.new name: "HTTPS", url: "https://example.com/webhook"
|
||||
assert webhook.valid?
|
||||
end
|
||||
|
||||
test "deactivate" do
|
||||
webhook = webhooks(:active)
|
||||
|
||||
assert_changes -> { webhook.active? }, from: true, to: false do
|
||||
webhook.deactivate
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user