Remove webhooks and related code (#15827)
This commit is contained in:
parent
ec9e9053d5
commit
06d6b68d9d
42 changed files with 5 additions and 838 deletions
1
Gemfile
1
Gemfile
|
|
@ -112,7 +112,6 @@ gem "stripe", "~> 5.42" # Ruby library for the Stripe API
|
|||
gem "strong_migrations", "~> 0.7" # Catch unsafe migrations
|
||||
gem "twitter", "~> 7.0" # A Ruby interface to the Twitter API
|
||||
gem "uglifier", "~> 4.2" # Uglifier minifies JavaScript files
|
||||
gem "ulid", "~> 1.3" # Universally Unique Lexicographically Sortable Identifier implementation for Ruby
|
||||
gem "validate_url", "~> 1.0" # Library for validating urls in Rails
|
||||
gem "vault", "~> 0.16" # Used to store secrets
|
||||
gem "view_component", "~> 2.47" # View components for Rails
|
||||
|
|
|
|||
|
|
@ -853,7 +853,6 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
ulid (1.3.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.8)
|
||||
|
|
@ -1059,7 +1058,6 @@ DEPENDENCIES
|
|||
timecop (~> 0.9)
|
||||
twitter (~> 7.0)
|
||||
uglifier (~> 4.2)
|
||||
ulid (~> 1.3)
|
||||
validate_url (~> 1.0)
|
||||
vault (~> 0.16)
|
||||
vcr (~> 6.0)
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
module Admin
|
||||
class WebhookEndpointsController < Admin::ApplicationController
|
||||
layout "admin"
|
||||
|
||||
def index
|
||||
@endpoints = Webhook::Endpoint.includes(:user)
|
||||
.page(params[:page]).per(50)
|
||||
.order(created_at: :desc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
module Api
|
||||
module V0
|
||||
class WebhooksController < ApiController
|
||||
before_action :authenticate!
|
||||
|
||||
skip_before_action :verify_authenticity_token, only: %w[create destroy]
|
||||
|
||||
ATTRIBUTES_FOR_SERIALIZATION = %i[id user_id source target_url events created_at].freeze
|
||||
private_constant :ATTRIBUTES_FOR_SERIALIZATION
|
||||
|
||||
def index
|
||||
@webhooks = webhooks_scope.order(:id)
|
||||
end
|
||||
|
||||
def create
|
||||
@webhook = @user.webhook_endpoints.new(webhook_params)
|
||||
@webhook.save!
|
||||
|
||||
render "show", status: :created
|
||||
end
|
||||
|
||||
def show
|
||||
@webhook = webhooks_scope.find(params[:id])
|
||||
end
|
||||
|
||||
def destroy
|
||||
webhook = webhooks_scope.find(params[:id])
|
||||
webhook.destroy!
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def webhooks_scope
|
||||
@user.webhook_endpoints.select(ATTRIBUTES_FOR_SERIALIZATION)
|
||||
end
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook_endpoint).permit(:target_url, :source, events: [])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -50,7 +50,6 @@ class AdminMenu
|
|||
item(name: "developer tools", controller: "tools", children: [
|
||||
item(name: "tools"),
|
||||
item(name: "vault secrets", controller: "secrets"),
|
||||
item(name: "webhooks", controller: "webhook_endpoints"),
|
||||
item(name: "data update scripts", visible: false),
|
||||
]),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -111,7 +111,6 @@ class User < ApplicationRecord
|
|||
foreign_key: :subscriber_id, inverse_of: :subscriber, dependent: :destroy
|
||||
has_many :subscribers, through: :source_authored_user_subscriptions, dependent: :destroy
|
||||
has_many :tweets, dependent: :nullify
|
||||
has_many :webhook_endpoints, class_name: "Webhook::Endpoint", inverse_of: :user, dependent: :delete_all
|
||||
has_many :devices, dependent: :delete_all
|
||||
has_many :sponsorships, dependent: :delete_all
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
module Webhook
|
||||
class Endpoint < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :webhook_endpoints
|
||||
|
||||
validates :events, presence: true
|
||||
validates :source, presence: true
|
||||
validates :target_url, presence: true, uniqueness: true, url: { schemes: %w[https] }
|
||||
validates :user_id, presence: true
|
||||
|
||||
attribute :events, :string, array: true, default: []
|
||||
|
||||
scope :for_events, ->(events) { where("events @> ARRAY[?]::varchar[]", Array(events)) }
|
||||
scope :for_app, ->(app_id) { where(oauth_application_id: app_id) }
|
||||
|
||||
def self.table_name_prefix
|
||||
"webhook_"
|
||||
end
|
||||
|
||||
def events=(events)
|
||||
events = Array(events).map { |event| event.to_s.underscore }
|
||||
super(Webhook::Event::EVENT_TYPES & events)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
module Webhook
|
||||
class Event
|
||||
EVENT_TYPES = %w[
|
||||
article_created
|
||||
article_updated
|
||||
article_destroyed
|
||||
].freeze
|
||||
|
||||
attr_reader :event_type, :payload, :timestamp, :event_id
|
||||
|
||||
def initialize(event_type:, payload: {})
|
||||
raise InvalidEvent unless EVENT_TYPES.include?(event_type)
|
||||
|
||||
@event_type = event_type
|
||||
@payload = payload
|
||||
|
||||
now = Time.current
|
||||
@timestamp = now.rfc3339
|
||||
@event_id = Secrets::Generator.sortable(now)
|
||||
end
|
||||
|
||||
def as_json(*_args)
|
||||
Webhook::EventSerializer.new(self).serializable_hash
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidEvent < StandardError; end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
class WebhookEndpoint < ApplicationRecord
|
||||
resourcify
|
||||
# This class exists to take advantage of Rolify for limiting authorization
|
||||
# on internal reports.
|
||||
# NOTE: It is not backed by a database table and should not be expected to
|
||||
# function like a traditional Rails model
|
||||
end
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
module Webhook
|
||||
class ArticleDestroyedSerializer < ApplicationSerializer
|
||||
set_type :article
|
||||
attributes :title
|
||||
end
|
||||
end
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
module Webhook
|
||||
class ArticleSerializer < ApplicationSerializer
|
||||
set_type :article
|
||||
attributes :title, :description, :readable_publish_date, :cached_tag_list, :cached_tag_list_array,
|
||||
:slug, :path, :url, :comments_count, :public_reactions_count, :body_markdown
|
||||
|
||||
attribute :canonical_url, &:processed_canonical_url
|
||||
attribute :body_html, &:processed_html
|
||||
attribute :created_at do |a|
|
||||
a.created_at.utc.iso8601
|
||||
end
|
||||
attribute :edited_at do |a|
|
||||
a.edited_at&.utc&.iso8601
|
||||
end
|
||||
attribute :crossposted_at do |a|
|
||||
a.crossposted_at&.utc&.iso8601
|
||||
end
|
||||
attribute :published_at do |a|
|
||||
a.published_at&.utc&.iso8601
|
||||
end
|
||||
attribute :last_comment_at do |a|
|
||||
a.last_comment_at&.utc&.iso8601
|
||||
end
|
||||
attribute :cover_image do |a|
|
||||
CloudCoverUrl.new(a.url).call
|
||||
end
|
||||
attribute :social_image do |article|
|
||||
Articles::SocialImage.new(article).url
|
||||
end
|
||||
attribute :user do |a|
|
||||
UserSerializer.new(a.user).serializable_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
module Webhook
|
||||
class EventSerializer < ApplicationSerializer
|
||||
set_type :webhook_event
|
||||
set_id :event_id
|
||||
|
||||
attributes :event_type, :timestamp, :payload
|
||||
end
|
||||
end
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
module Webhook
|
||||
class UserSerializer < ApplicationSerializer
|
||||
attributes :name, :username, :twitter_username, :github_username
|
||||
attribute :website_url, &:processed_website_url
|
||||
attribute :profile_image do |user|
|
||||
Images::Profile.call(user.profile_image_url, length: 640)
|
||||
end
|
||||
attribute :profile_image_90 do |user|
|
||||
Images::Profile.call(user.profile_image_url, length: 90)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
module Articles
|
||||
class Creator
|
||||
def initialize(user, article_params, event_dispatcher = Webhook::DispatchEvent)
|
||||
def initialize(user, article_params)
|
||||
@user = user
|
||||
@article_params = article_params
|
||||
@event_dispatcher = event_dispatcher
|
||||
end
|
||||
|
||||
def self.call(...)
|
||||
|
|
@ -22,7 +21,6 @@ module Articles
|
|||
|
||||
# Send notifications to any mentioned users, followed by any users who follow the article's author.
|
||||
Notification.send_to_mentioned_users_and_followers(article) if article.published?
|
||||
dispatch_event(article)
|
||||
end
|
||||
|
||||
article
|
||||
|
|
@ -30,7 +28,7 @@ module Articles
|
|||
|
||||
private
|
||||
|
||||
attr_reader :user, :article_params, :event_dispatcher
|
||||
attr_reader :user, :article_params
|
||||
|
||||
def rate_limit!
|
||||
rate_limit_to_use = if user.decorate.considered_new?
|
||||
|
|
@ -42,12 +40,6 @@ module Articles
|
|||
user.rate_limiter.check_limit!(rate_limit_to_use)
|
||||
end
|
||||
|
||||
def dispatch_event(article)
|
||||
return unless article.published?
|
||||
|
||||
event_dispatcher.call("article_created", article)
|
||||
end
|
||||
|
||||
def save_article
|
||||
series = article_params[:series]
|
||||
tags = article_params[:tags]
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ module Articles
|
|||
module Destroyer
|
||||
module_function
|
||||
|
||||
def call(article, event_dispatcher = Webhook::DispatchEvent)
|
||||
def call(article)
|
||||
# comments will automatically lose the connection to their article once `.destroy` is called,
|
||||
# due to the `dependent: nullify` clause, so to remove their notifications,
|
||||
# we need to cache the ids in advance
|
||||
|
|
@ -15,8 +15,6 @@ module Articles
|
|||
if article_comments_ids.present?
|
||||
Notification.remove_all(notifiable_ids: article_comments_ids, notifiable_type: "Comment")
|
||||
end
|
||||
|
||||
event_dispatcher.call("article_destroyed", article) if article.published?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,11 +2,10 @@ module Articles
|
|||
class Updater
|
||||
Result = Struct.new(:success, :article, keyword_init: true)
|
||||
|
||||
def initialize(user, article, article_params, event_dispatcher = Webhook::DispatchEvent)
|
||||
def initialize(user, article, article_params)
|
||||
@user = user
|
||||
@article = article
|
||||
@article_params = article_params
|
||||
@event_dispatcher = event_dispatcher
|
||||
end
|
||||
|
||||
def self.call(...)
|
||||
|
|
@ -16,9 +15,6 @@ module Articles
|
|||
def call
|
||||
user.rate_limiter.check_limit!(:article_update)
|
||||
|
||||
# Grab the state of the article's "publish" status before making any further updates to it.
|
||||
was_previously_published = article.published
|
||||
|
||||
# updated edited time only if already published and not edited by an admin
|
||||
update_edited_at = article.user == user && article.published
|
||||
attrs = Articles::Attributes.new(article_params, article.user).for_update(update_edited_at: update_edited_at)
|
||||
|
|
@ -52,19 +48,12 @@ module Articles
|
|||
notifiable_type: "Mention")
|
||||
end
|
||||
end
|
||||
|
||||
# Do not notify if the article was previously already in a published state or is continually unpublished.
|
||||
dispatch_event(article) if article.published || was_previously_published
|
||||
end
|
||||
Result.new(success: success, article: article.decorate)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :article, :article_params, :event_dispatcher
|
||||
|
||||
def dispatch_event(article)
|
||||
event_dispatcher.call("article_updated", article)
|
||||
end
|
||||
attr_reader :user, :article, :article_params
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
module Secrets
|
||||
module Generator
|
||||
module_function
|
||||
|
||||
# Generates a unique and lexicographically sortable ID
|
||||
def sortable(time = Time.current)
|
||||
ULID.generate(time)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -9,7 +9,6 @@ module Users
|
|||
user.created_podcasts.update_all(creator_id: nil)
|
||||
user.blocker_blocks.delete_all
|
||||
user.blocked_blocks.delete_all
|
||||
user.webhook_endpoints.delete_all
|
||||
user.authored_notes.delete_all
|
||||
user.display_ad_events.delete_all
|
||||
user.email_messages.delete_all
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
module Webhook
|
||||
class DispatchEvent
|
||||
def initialize(event_type, record)
|
||||
@event_type = event_type
|
||||
@record = record
|
||||
end
|
||||
|
||||
def self.call(...)
|
||||
new(...).call
|
||||
end
|
||||
|
||||
def call
|
||||
endpoint_urls = Endpoint.for_events([event_type]).where(user_id: record.user_id).pluck(:target_url)
|
||||
return if endpoint_urls.empty?
|
||||
|
||||
event_json = Event.new(event_type: event_type, payload: PayloadAdapter.new(record).hash).to_json
|
||||
endpoint_urls.each do |url|
|
||||
DispatchEventWorker.perform_async(url, event_json)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :event_type, :record
|
||||
end
|
||||
end
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
module Webhook
|
||||
class PayloadAdapter
|
||||
def initialize(object)
|
||||
raise InvalidPayloadObject unless object.is_a?(Article)
|
||||
|
||||
@object = object
|
||||
end
|
||||
|
||||
def hash
|
||||
serializer.new(prepared_object).serializable_hash
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :object
|
||||
|
||||
# decorate article before serializing
|
||||
def prepared_object
|
||||
return object unless object.is_a?(Article) && !object.decorated?
|
||||
|
||||
object.decorate
|
||||
end
|
||||
|
||||
def serializer
|
||||
object.destroyed? ? ArticleDestroyedSerializer : ArticleSerializer
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidPayloadObject < StandardError; end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
json.array! @webhooks.each do |endpoint|
|
||||
json.type_of "webhook_endpoint"
|
||||
|
||||
json.extract!(endpoint, :id, :source, :target_url, :events)
|
||||
|
||||
json.created_at endpoint.created_at.rfc3339
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
json.type_of "webhook_endpoint"
|
||||
|
||||
json.call(@webhook, :id, :source, :target_url, :events)
|
||||
|
||||
json.created_at @webhook.created_at.rfc3339
|
||||
|
||||
json.partial! "api/v0/shared/user", user: @webhook.user
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
module Webhook
|
||||
class DestroyWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :low_priority, retry: 10
|
||||
|
||||
def perform(user_id, application_id)
|
||||
Webhook::Endpoint.destroy_by(user_id: user_id, oauth_application_id: application_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
module Webhook
|
||||
class DispatchEventWorker
|
||||
include Sidekiq::Worker
|
||||
|
||||
sidekiq_options queue: :medium_priority
|
||||
|
||||
def perform(endpoint_url, event_json)
|
||||
uri = Addressable::URI.parse(endpoint_url)
|
||||
HTTParty.post(uri, headers: { "Content-Type" => "application/json" }, body: event_json, timeout: 10)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -64,7 +64,6 @@ Rails.application.routes.draw do
|
|||
get :organizations
|
||||
end
|
||||
resources :readinglist, only: [:index]
|
||||
resources :webhooks, only: %i[index create show destroy]
|
||||
|
||||
resources :listings, only: %i[index show create update]
|
||||
get "/listings/category/:category", to: "listings#index", as: :listings_category
|
||||
|
|
|
|||
|
|
@ -143,7 +143,6 @@ namespace :admin do
|
|||
post "bust_cache"
|
||||
end
|
||||
end
|
||||
resources :webhook_endpoints, only: :index
|
||||
|
||||
# We do not expose the Data Update Scripts to all Forems by default.
|
||||
constraints(->(_request) { FeatureFlag.enabled?(:data_update_scripts) }) do
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
FactoryBot.define do
|
||||
factory :webhook_endpoint, class: "Webhook::Endpoint" do
|
||||
target_url { Faker::Internet.url(scheme: "https") }
|
||||
events { Webhook::Event::EVENT_TYPES }
|
||||
user
|
||||
source { "stackbit" }
|
||||
end
|
||||
end
|
||||
|
|
@ -110,7 +110,6 @@ RSpec.describe User, type: :model do
|
|||
it { is_expected.to have_many(:subscribed_to_user_subscriptions).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:subscribers).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:tweets).dependent(:nullify) }
|
||||
it { is_expected.to have_many(:webhook_endpoints).class_name("Webhook::Endpoint").dependent(:delete_all) }
|
||||
|
||||
# rubocop:disable RSpec/NamedSubject
|
||||
it do
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::Endpoint, type: :model do
|
||||
let!(:endpoint) do
|
||||
create(
|
||||
:webhook_endpoint, user: user, events: %w[article_created article_updated article_destroyed]
|
||||
)
|
||||
end
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe "validations" do
|
||||
it { is_expected.to belong_to(:user).inverse_of(:webhook_endpoints) }
|
||||
|
||||
it { is_expected.to validate_presence_of(:events) }
|
||||
it { is_expected.to validate_presence_of(:source) }
|
||||
it { is_expected.to validate_presence_of(:target_url) }
|
||||
it { is_expected.to validate_presence_of(:user_id) }
|
||||
|
||||
it { is_expected.to validate_uniqueness_of(:target_url) }
|
||||
|
||||
it { is_expected.to allow_value("https://foo.com").for(:target_url) }
|
||||
it { is_expected.not_to allow_value("http://foo.com").for(:target_url) }
|
||||
end
|
||||
|
||||
it "is valid" do
|
||||
expect(endpoint).to be_valid
|
||||
end
|
||||
|
||||
it "sets events according to the list" do
|
||||
webhook = create(
|
||||
:webhook_endpoint, events: %w[article_updated other_updated cool article_created]
|
||||
)
|
||||
webhook.reload
|
||||
expect(webhook.events.sort).to eq(%w[article_created article_updated])
|
||||
end
|
||||
|
||||
context "when endpoints exist" do
|
||||
let!(:epoint2) { create(:webhook_endpoint, events: %w[article_destroyed], user: user) }
|
||||
let!(:epoint3) { create(:webhook_endpoint, events: %w[article_updated article_destroyed]) }
|
||||
|
||||
before do
|
||||
create(:webhook_endpoint, events: %w[article_created])
|
||||
end
|
||||
|
||||
it "finds for events" do
|
||||
d_points = described_class.for_events("article_destroyed")
|
||||
expect(d_points.ids.sort).to eq([endpoint, epoint2, epoint3].map(&:id).sort)
|
||||
end
|
||||
|
||||
it "finds for_events array" do
|
||||
endpoints = described_class.for_events(%w[article_created article_destroyed])
|
||||
expect(endpoints.ids).to eq([endpoint.id])
|
||||
end
|
||||
|
||||
it "belongs to user" do
|
||||
expect(user.webhook_endpoints.ids.sort).to eq([endpoint, epoint2].map(&:id).sort)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::Event, type: :model do
|
||||
let(:article) { create(:article) }
|
||||
let(:payload) { Webhook::PayloadAdapter.new(article).hash }
|
||||
|
||||
describe "validations" do
|
||||
it "raises an exception with a unknown event type" do
|
||||
expect do
|
||||
described_class.new(event_type: "cool_event")
|
||||
end.to raise_error(Webhook::InvalidEvent)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#as_json" do
|
||||
it "provides correct json (sample)" do
|
||||
event = described_class.new(event_type: "article_created", payload: { title: "Hello, world" })
|
||||
hash = event.as_json
|
||||
|
||||
attributes = hash[:data][:attributes]
|
||||
|
||||
expect(attributes[:event_type]).to eq("article_created")
|
||||
expect(attributes[:timestamp]).to be_truthy
|
||||
expect(attributes[:payload][:title]).to eq("Hello, world")
|
||||
end
|
||||
|
||||
it "provides correct json including article" do
|
||||
event = described_class.new(event_type: "article_updated", payload: payload)
|
||||
hash = event.as_json
|
||||
attributes = hash[:data][:attributes]
|
||||
expect(attributes[:event_type]).to eq("article_updated")
|
||||
expect(attributes[:payload][:data][:attributes][:title]).to eq(article.title)
|
||||
end
|
||||
|
||||
it "provides an event_id dependent on time" do
|
||||
event1 = described_class.new(event_type: "article_updated", payload: payload)
|
||||
event1_id = event1.as_json.dig(:data, :id)
|
||||
|
||||
event2 = nil
|
||||
Timecop.freeze(1.month.ago) do
|
||||
event2 = described_class.new(event_type: "article_updated", payload: payload)
|
||||
end
|
||||
event2_id = event2.as_json.dig(:data, :id)
|
||||
|
||||
expect(event2_id < event1_id).to be(true)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#to_json" do
|
||||
it "provides correct json including article" do
|
||||
event = described_class.new(event_type: "article_updated", payload: payload)
|
||||
json = event.to_json
|
||||
hash = JSON.parse(json)
|
||||
attributes = hash["data"]["attributes"]
|
||||
|
||||
expect(attributes["event_type"]).to eq("article_updated")
|
||||
expect(attributes["payload"]["data"]["attributes"]["title"]).to eq(article.title)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Api::V0::Webhooks", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let!(:webhook) do
|
||||
create(:webhook_endpoint, user: user, target_url: "https://api.example.com/go")
|
||||
end
|
||||
|
||||
describe "GET /api/v0/webhooks" do
|
||||
let(:webhook2) do
|
||||
create(:webhook_endpoint, user: user, target_url: "https://api.example.com/webhook")
|
||||
end
|
||||
|
||||
context "when accessing via cookie" do
|
||||
before do
|
||||
sign_in user
|
||||
create(:webhook_endpoint)
|
||||
end
|
||||
|
||||
it "returns 200 on success" do
|
||||
get api_webhooks_path
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "returns json on success" do
|
||||
webhook2
|
||||
get api_webhooks_path
|
||||
|
||||
expect(response.parsed_body).to include(
|
||||
hash_including("id" => webhook.id, "target_url" => webhook.target_url),
|
||||
hash_including("id" => webhook2.id, "target_url" => webhook2.target_url),
|
||||
)
|
||||
end
|
||||
|
||||
it "returns the correct json representation" do
|
||||
get api_webhooks_path
|
||||
|
||||
expect(response.parsed_body.first).to eq(
|
||||
"created_at" => webhook.created_at.rfc3339,
|
||||
"events" => webhook.events,
|
||||
"id" => webhook.id,
|
||||
"source" => webhook.source,
|
||||
"target_url" => webhook.target_url,
|
||||
"type_of" => "webhook_endpoint",
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /api/v0/webhooks/:id" do
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "returns 200 on success" do
|
||||
get api_webhook_path(webhook.id)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "returns 404 if the webhook does not exist" do
|
||||
get api_webhook_path(9999)
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns 404 if another user webhook is accessed" do
|
||||
other_webhook = create(:webhook_endpoint, user: create(:user))
|
||||
|
||||
get api_webhook_path(other_webhook.id)
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it "returns the correct json representation" do
|
||||
get api_webhook_path(webhook.id)
|
||||
|
||||
expect(response.parsed_body).to include(
|
||||
"created_at" => webhook.created_at.rfc3339,
|
||||
"events" => webhook.events,
|
||||
"id" => webhook.id,
|
||||
"source" => webhook.source,
|
||||
"target_url" => webhook.target_url,
|
||||
"type_of" => "webhook_endpoint",
|
||||
)
|
||||
end
|
||||
|
||||
it "returns the correct json representation for the webhook user" do
|
||||
get api_webhook_path(webhook.id)
|
||||
|
||||
response_webhook_user = response.parsed_body["user"]
|
||||
|
||||
expect(response_webhook_user).to eq({
|
||||
"name" => webhook.user.name,
|
||||
"username" => webhook.user.username,
|
||||
"twitter_username" => webhook.user.twitter_username,
|
||||
"github_username" => webhook.user.github_username,
|
||||
"website_url" => webhook.user.processed_website_url,
|
||||
"profile_image" => Images::Profile.call(webhook.user.profile_image_url,
|
||||
length: 640),
|
||||
"profile_image_90" => Images::Profile.call(webhook.user.profile_image_url,
|
||||
length: 90)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /api/v0/webhooks" do
|
||||
let(:webhook_params) do
|
||||
{
|
||||
source: "stackbit",
|
||||
target_url: Faker::Internet.url(scheme: "https"),
|
||||
events: %w[article_created article_updated article_destroyed]
|
||||
}
|
||||
end
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "creates a webhook" do
|
||||
expect do
|
||||
post api_webhooks_path, params: { webhook_endpoint: webhook_params }
|
||||
end.to change(Webhook::Endpoint, :count).by(1)
|
||||
end
|
||||
|
||||
it "creates a webhook with events and data" do
|
||||
post api_webhooks_path, params: { webhook_endpoint: webhook_params }
|
||||
created_webhook = user.webhook_endpoints.last
|
||||
expect(created_webhook.events).to eq(%w[article_created article_updated article_destroyed])
|
||||
expect(created_webhook.target_url).to eq(webhook_params[:target_url])
|
||||
expect(created_webhook.source).to eq(webhook_params[:source])
|
||||
expect(created_webhook.oauth_application_id).to eq(nil)
|
||||
end
|
||||
|
||||
it "returns :created and json response on success" do
|
||||
post api_webhooks_path, params: { webhook_endpoint: webhook_params }
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(response.media_type).to eq("application/json")
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["target_url"]).to eq(webhook_params[:target_url])
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /api/v0/webhooks/:id" do
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it "deletes the webhook" do
|
||||
expect do
|
||||
delete api_webhook_path(webhook.id)
|
||||
end.to change(Webhook::Endpoint, :count).by(-1)
|
||||
end
|
||||
|
||||
it "returns 204 on success" do
|
||||
delete api_webhook_path(webhook.id)
|
||||
expect(response).to have_http_status(:no_content)
|
||||
end
|
||||
|
||||
it "doesn't allow to destroy other user webhook" do
|
||||
other_webhook = create(:webhook_endpoint, user: create(:user))
|
||||
expect do
|
||||
delete api_webhook_path(other_webhook.id)
|
||||
end.not_to change(Webhook::Endpoint, :count)
|
||||
end
|
||||
|
||||
it "returns 404 if another user webhook is accessed" do
|
||||
other_webhook = create(:webhook_endpoint, user: create(:user))
|
||||
delete api_webhook_path(other_webhook.id)
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -77,24 +77,6 @@ RSpec.describe "ArticlesCreate", type: :request do
|
|||
}
|
||||
end
|
||||
|
||||
before do
|
||||
create(:webhook_endpoint, events: %w[article_created article_updated], target_url: url, user: user)
|
||||
end
|
||||
|
||||
it "doesn't schedule a dispatching event job (unpublished)" do
|
||||
sidekiq_assert_no_enqueued_jobs(only: Webhook::DispatchEventWorker) do
|
||||
post "/articles", params: article_params
|
||||
end
|
||||
end
|
||||
|
||||
it "schedules a dispatching event job (published)" do
|
||||
body_markdown = "---\ntitle: hey hey hahuu\npublished: true\nseries: helloyo\n---\nYo ho ho#{rand(100)}"
|
||||
article_params[:article][:body_markdown] = body_markdown
|
||||
sidekiq_assert_enqueued_jobs(1, only: Webhook::DispatchEventWorker) do
|
||||
post "/articles", params: article_params
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't fail when executing jobs" do
|
||||
stub_request(:post, url).to_return(status: 200)
|
||||
sidekiq_perform_enqueued_jobs do
|
||||
|
|
|
|||
|
|
@ -167,13 +167,4 @@ RSpec.describe "ArticlesUpdate", type: :request do
|
|||
expect(response).to redirect_to "#{article.path}/edit"
|
||||
expect(article.reload.video_thumbnail_url).to include "https://i.imgur.com/HPiu7N4.jpg"
|
||||
end
|
||||
|
||||
it "schedules a dispatching event job" do
|
||||
create(:webhook_endpoint, events: %w[article_created article_updated], user: user)
|
||||
sidekiq_assert_enqueued_jobs(1, only: Webhook::DispatchEventWorker) do
|
||||
put "/articles/#{article.id}", params: {
|
||||
article: { title: "new_title", body_markdown: "Yo ho ho#{rand(100)}", tag_list: "yo" }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,21 +38,6 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
described_class.call(user, valid_attributes)
|
||||
end.to change(NotificationSubscription, :count).by(1)
|
||||
end
|
||||
|
||||
it "calls an event dispatcher" do
|
||||
event_dispatcher = double
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
article = described_class.call(user, valid_attributes, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_created", article)
|
||||
end
|
||||
|
||||
it "doesn't call an event dispatcher when an article is unpublished" do
|
||||
attributes = attributes_for(:article, published: false)
|
||||
event_dispatcher = double
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
article = described_class.call(user, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).not_to have_received(:call).with("article_created", article)
|
||||
end
|
||||
end
|
||||
|
||||
context "when invalid attributes" do
|
||||
|
|
@ -94,12 +79,5 @@ RSpec.describe Articles::Creator, type: :service do
|
|||
described_class.call(user, invalid_body_attributes)
|
||||
end.not_to change(NotificationSubscription, :count)
|
||||
end
|
||||
|
||||
it "doesn't call an event dispatcher" do
|
||||
event_dispatcher = double
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
described_class.call(user, invalid_body_attributes, event_dispatcher)
|
||||
expect(event_dispatcher).not_to have_received(:call)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@ RSpec.describe Articles::Destroyer, type: :service do
|
|||
let(:article) { create(:article) }
|
||||
let(:event_dispatcher) { double }
|
||||
|
||||
before do
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
end
|
||||
|
||||
it "destroys an article" do
|
||||
described_class.call(article)
|
||||
expect(Article.find_by(id: article.id)).to be_nil
|
||||
|
|
@ -19,15 +15,4 @@ RSpec.describe Articles::Destroyer, type: :service do
|
|||
described_class.call(article)
|
||||
end
|
||||
end
|
||||
|
||||
it "calls events dispatcher" do
|
||||
described_class.call(article, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_destroyed", article)
|
||||
end
|
||||
|
||||
it "doesn't call a dispatched when a draft is destroyed" do
|
||||
draft = create(:article, published: false)
|
||||
described_class.call(draft, event_dispatcher)
|
||||
expect(event_dispatcher).not_to have_received(:call)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -138,34 +138,4 @@ RSpec.describe Articles::Updater, type: :service do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "events dispatcher" do
|
||||
let(:event_dispatcher) { double }
|
||||
|
||||
before do
|
||||
allow(event_dispatcher).to receive(:call)
|
||||
end
|
||||
|
||||
it "calls the dispatcher" do
|
||||
described_class.call(user, article, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_updated", article)
|
||||
end
|
||||
|
||||
it "doesn't call the dispatcher when unpublished => unpublished" do
|
||||
described_class.call(user, draft, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).not_to have_received(:call)
|
||||
end
|
||||
|
||||
it "calls the dispatcher when unpublished => published" do
|
||||
attributes[:published] = true
|
||||
described_class.call(user, draft, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_updated", draft)
|
||||
end
|
||||
|
||||
it "calls the dispatcher when published => unpublished" do
|
||||
attributes[:published] = false
|
||||
described_class.call(user, article, attributes, event_dispatcher)
|
||||
expect(event_dispatcher).to have_received(:call).with("article_updated", article)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Secrets::Generator, type: :service do
|
||||
describe ".sortable" do
|
||||
it "generates unique identifiers" do
|
||||
expect(described_class.sortable).not_to eq(described_class.sortable)
|
||||
end
|
||||
|
||||
it "generates sortable identifiers" do
|
||||
now_id = described_class.sortable
|
||||
a_month_from_now_id = described_class.sortable(1.month.from_now)
|
||||
a_month_ago_id = described_class.sortable(1.month.ago)
|
||||
|
||||
expected_ids = [a_month_ago_id, now_id, a_month_from_now_id]
|
||||
expect([now_id, a_month_from_now_id, a_month_ago_id].sort).to eq(expected_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::DispatchEvent, type: :service do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:article) { create(:article, user: user) }
|
||||
|
||||
it "does nothing if there are no corresponding endpoints" do
|
||||
create(:webhook_endpoint, events: %w[article_created], user: user)
|
||||
sidekiq_assert_no_enqueued_jobs(only: Webhook::DispatchEventWorker) do
|
||||
described_class.call("article_destroyed", article)
|
||||
end
|
||||
end
|
||||
|
||||
it "schedules jobs" do
|
||||
create(:webhook_endpoint, events: %w[article_created], user: user,
|
||||
target_url: "https://create-webhooks.example.com/accept")
|
||||
create(:webhook_endpoint, events: %w[article_created article_updated article_destroyed], user: user,
|
||||
target_url: "https://all-webhooks.example.com/accept")
|
||||
create(:webhook_endpoint, events: %w[article_destroyed], user: user,
|
||||
target_url: "https://destroy-webhooks.example.com/accept")
|
||||
sidekiq_assert_enqueued_jobs(2, only: Webhook::DispatchEventWorker) do
|
||||
described_class.call("article_created", article)
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't schedule jobs if the endpoints belong to another user" do
|
||||
user2 = create(:user)
|
||||
create(:webhook_endpoint, events: %w[article_created], user: user2,
|
||||
target_url: "https://create-webhooks.example.com/accept")
|
||||
sidekiq_assert_no_enqueued_jobs(only: Webhook::DispatchEventWorker) do
|
||||
described_class.call("article_created", article)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::PayloadAdapter, type: :service do
|
||||
it "raises an exception when invalid object is passed" do
|
||||
expect do
|
||||
described_class.new(User.new).hash
|
||||
end.to raise_error(Webhook::InvalidPayloadObject)
|
||||
end
|
||||
|
||||
describe "#hash" do
|
||||
let(:user) { create(:user) }
|
||||
let!(:article) { create(:article, title: "I'm super", user: user) }
|
||||
|
||||
it "returns a hash for a persisted article" do
|
||||
data = described_class.new(article).hash
|
||||
expect(data).to be_kind_of(Hash)
|
||||
expect(data[:data][:attributes][:title]).to eq(article.title)
|
||||
expect(data[:data][:attributes][:body_markdown]).to be_truthy
|
||||
end
|
||||
|
||||
it "returns a hash with a user" do
|
||||
data = described_class.new(article).hash
|
||||
expect(data[:data][:attributes][:user][:data][:attributes][:username]).to eq(user.username)
|
||||
end
|
||||
|
||||
it "returns a hash for a destroyed article" do
|
||||
article = create(:article, title: "hello")
|
||||
article.destroy
|
||||
data = described_class.new(article).hash
|
||||
expect(data).to be_kind_of(Hash)
|
||||
expect(data[:data][:attributes][:title]).to eq("hello")
|
||||
expect(data[:data][:attributes][:body_markdown]).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::DestroyWorker, type: :worker do
|
||||
let(:user) { create(:user) }
|
||||
let!(:webhook_endpoint) { create(:webhook_endpoint, user: user) }
|
||||
let!(:other_webhook_endpoint) { create(:webhook_endpoint) }
|
||||
let(:worker) { subject }
|
||||
|
||||
describe "#perform_now" do
|
||||
xit "destroys webhook by user_id and app_id" do
|
||||
worker.perform(user.id)
|
||||
expect(Webhook::Endpoint.find_by(id: webhook_endpoint.id)).to be_nil
|
||||
expect(other_webhook_endpoint.reload).to be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::DispatchEventWorker, type: :worker do
|
||||
let(:worker) { subject }
|
||||
|
||||
include_examples "#enqueues_on_correct_queue", "medium_priority", ["https://example.com", {}.to_json]
|
||||
|
||||
describe "#perform_now" do
|
||||
let(:article) { create(:article) }
|
||||
let(:payload) { Webhook::PayloadAdapter.new(article).hash }
|
||||
let(:json) { Webhook::Event.new(event_type: "article_updated", payload: payload).to_json }
|
||||
let(:url) { Faker::Internet.url }
|
||||
let(:client) { HTTParty }
|
||||
|
||||
it "posts an event" do
|
||||
allow(client).to receive(:post)
|
||||
worker.perform(url, json)
|
||||
expect(client).to have_received(:post).once
|
||||
.with(Addressable::URI.parse(url), headers: { "Content-Type" => "application/json" },
|
||||
body: json,
|
||||
timeout: 10)
|
||||
end
|
||||
|
||||
it "doesn't fail" do
|
||||
stub_request(:post, url).to_return(status: 200)
|
||||
worker.perform(url, json)
|
||||
end
|
||||
end
|
||||
end
|
||||
BIN
vendor/cache/ulid-1.3.0.gem
vendored
BIN
vendor/cache/ulid-1.3.0.gem
vendored
Binary file not shown.
Loading…
Add table
Reference in a new issue