parent
917bc14eae
commit
bfdf636dcf
11 changed files with 257 additions and 35 deletions
29
app/controllers/api/v0/webhooks_controller.rb
Normal file
29
app/controllers/api/v0/webhooks_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
module Api
|
||||
module V0
|
||||
class WebhooksController < ApiController
|
||||
respond_to :json
|
||||
before_action :authenticate!
|
||||
|
||||
def create
|
||||
@webhook = current_user.webhook_endpoints.create!(webhook_params)
|
||||
render "show", status: :created
|
||||
end
|
||||
|
||||
def show
|
||||
@webhook = Webhook::Endpoint.includes(:user).find(params[:id])
|
||||
end
|
||||
|
||||
def destroy
|
||||
webhook = current_user.webhook_endpoints.find(params[:id])
|
||||
webhook.destroy
|
||||
render json: { success: webhook.destroyed? }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def webhook_params
|
||||
params.require(:webhook_endpoint).permit(:target_url, :source, events: [])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -13,41 +13,42 @@ class User < ApplicationRecord
|
|||
acts_as_followable
|
||||
acts_as_follower
|
||||
|
||||
has_many :organization_memberships, dependent: :destroy
|
||||
has_many :organizations, through: :organization_memberships
|
||||
has_many :api_secrets, dependent: :destroy
|
||||
has_many :articles, dependent: :destroy
|
||||
has_many :badge_achievements, dependent: :destroy
|
||||
has_many :badges, through: :badge_achievements
|
||||
has_many :collections, dependent: :destroy
|
||||
has_many :comments, dependent: :destroy
|
||||
has_many :email_messages, class_name: "Ahoy::Message"
|
||||
has_many :github_repos, dependent: :destroy
|
||||
has_many :identities, dependent: :destroy
|
||||
has_many :mentions, dependent: :destroy
|
||||
has_many :messages, dependent: :destroy
|
||||
has_many :notes, as: :noteable, inverse_of: :noteable
|
||||
has_many :profile_pins, as: :profile, inverse_of: :profile
|
||||
has_many :authored_notes, as: :author, inverse_of: :author, class_name: "Note"
|
||||
has_many :notifications, dependent: :destroy
|
||||
has_many :reactions, dependent: :destroy
|
||||
has_many :tweets, dependent: :destroy
|
||||
has_many :chat_channel_memberships, dependent: :destroy
|
||||
has_many :chat_channels, through: :chat_channel_memberships
|
||||
has_many :notification_subscriptions, dependent: :destroy
|
||||
has_many :push_notification_subscriptions, dependent: :destroy
|
||||
has_many :feedback_messages
|
||||
has_many :rating_votes
|
||||
has_many :html_variants, dependent: :destroy
|
||||
has_many :page_views
|
||||
has_many :credits
|
||||
has_many :classified_listings
|
||||
has_many :poll_votes
|
||||
has_many :poll_skips
|
||||
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData"
|
||||
has_many :display_ad_events
|
||||
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
||||
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
||||
has_many :organization_memberships, dependent: :destroy
|
||||
has_many :organizations, through: :organization_memberships
|
||||
has_many :api_secrets, dependent: :destroy
|
||||
has_many :articles, dependent: :destroy
|
||||
has_many :badge_achievements, dependent: :destroy
|
||||
has_many :badges, through: :badge_achievements
|
||||
has_many :collections, dependent: :destroy
|
||||
has_many :comments, dependent: :destroy
|
||||
has_many :email_messages, class_name: "Ahoy::Message"
|
||||
has_many :github_repos, dependent: :destroy
|
||||
has_many :identities, dependent: :destroy
|
||||
has_many :mentions, dependent: :destroy
|
||||
has_many :messages, dependent: :destroy
|
||||
has_many :notes, as: :noteable, inverse_of: :noteable
|
||||
has_many :profile_pins, as: :profile, inverse_of: :profile
|
||||
has_many :authored_notes, as: :author, inverse_of: :author, class_name: "Note"
|
||||
has_many :notifications, dependent: :destroy
|
||||
has_many :reactions, dependent: :destroy
|
||||
has_many :tweets, dependent: :destroy
|
||||
has_many :chat_channel_memberships, dependent: :destroy
|
||||
has_many :chat_channels, through: :chat_channel_memberships
|
||||
has_many :notification_subscriptions, dependent: :destroy
|
||||
has_many :push_notification_subscriptions, dependent: :destroy
|
||||
has_many :feedback_messages
|
||||
has_many :rating_votes
|
||||
has_many :html_variants, dependent: :destroy
|
||||
has_many :page_views
|
||||
has_many :credits
|
||||
has_many :classified_listings
|
||||
has_many :poll_votes
|
||||
has_many :poll_skips
|
||||
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData"
|
||||
has_many :display_ad_events
|
||||
has_many :access_grants, class_name: "Doorkeeper::AccessGrant", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
||||
has_many :access_tokens, class_name: "Doorkeeper::AccessToken", foreign_key: :resource_owner_id, inverse_of: :resource_owner, dependent: :delete_all
|
||||
has_many :webhook_endpoints, class_name: "Webhook::Endpoint", foreign_key: :user_id, inverse_of: :user, dependent: :delete_all
|
||||
|
||||
mount_uploader :profile_image, ProfileImageUploader
|
||||
|
||||
|
|
|
|||
21
app/models/webhook/endpoint.rb
Normal file
21
app/models/webhook/endpoint.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
module Webhook
|
||||
class Endpoint < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :webhook_endpoints
|
||||
|
||||
validates :target_url, uniqueness: true, url: { schemes: %w[https] }
|
||||
validates :events, presence: true
|
||||
|
||||
attribute :events, :string, array: true, default: []
|
||||
|
||||
scope :for_events, ->(events) { where("events @> ARRAY[?]::varchar[]", Array(events)) }
|
||||
|
||||
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
|
||||
18
app/models/webhook/event.rb
Normal file
18
app/models/webhook/event.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
module Webhook
|
||||
class Event
|
||||
EVENT_TYPES = %w[
|
||||
article_created
|
||||
article_updated
|
||||
article_destroyed
|
||||
].freeze
|
||||
|
||||
def initialize(event_name, payload = {})
|
||||
@event_name = event_name
|
||||
@payload = payload
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :event_name, :payload
|
||||
end
|
||||
end
|
||||
6
app/views/api/v0/webhooks/show.json.jbuilder
Normal file
6
app/views/api/v0/webhooks/show.json.jbuilder
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
json.type_of "webhook_endpoint"
|
||||
json.events @webhook.events
|
||||
json.target_url @webhook.target_url
|
||||
json.source @webhook.source
|
||||
|
||||
json.partial! "api/v0/articles/user", user: @webhook.user
|
||||
|
|
@ -107,6 +107,7 @@ Rails.application.routes.draw do
|
|||
post "/update_or_create", to: "github_repos#update_or_create"
|
||||
end
|
||||
end
|
||||
resources :webhooks, only: %i[create show destroy]
|
||||
|
||||
get "/analytics/totals", to: "analytics#totals"
|
||||
get "/analytics/historical", to: "analytics#historical"
|
||||
|
|
|
|||
12
db/migrate/20190819104106_create_webhook_endpoints.rb
Normal file
12
db/migrate/20190819104106_create_webhook_endpoints.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class CreateWebhookEndpoints < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :webhook_endpoints do |t|
|
||||
t.string :target_url, null: false
|
||||
t.string :events, null: false, array: true
|
||||
t.references :user, foreign_key: true, null: false
|
||||
t.string :source
|
||||
t.timestamps
|
||||
t.index :events
|
||||
end
|
||||
end
|
||||
end
|
||||
12
db/schema.rb
12
db/schema.rb
|
|
@ -1149,6 +1149,17 @@ ActiveRecord::Schema.define(version: 2019_08_27_163358) do
|
|||
t.index ["user_id", "role_id"], name: "index_users_roles_on_user_id_and_role_id"
|
||||
end
|
||||
|
||||
create_table "webhook_endpoints", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.string "events", null: false, array: true
|
||||
t.string "source"
|
||||
t.string "target_url", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.index ["events"], name: "index_webhook_endpoints_on_events"
|
||||
t.index ["user_id"], name: "index_webhook_endpoints_on_user_id"
|
||||
end
|
||||
|
||||
add_foreign_key "badge_achievements", "badges"
|
||||
add_foreign_key "badge_achievements", "users"
|
||||
add_foreign_key "chat_channel_memberships", "chat_channels"
|
||||
|
|
@ -1162,4 +1173,5 @@ ActiveRecord::Schema.define(version: 2019_08_27_163358) do
|
|||
add_foreign_key "push_notification_subscriptions", "users"
|
||||
add_foreign_key "sponsorships", "organizations"
|
||||
add_foreign_key "sponsorships", "users"
|
||||
add_foreign_key "webhook_endpoints", "users"
|
||||
end
|
||||
|
|
|
|||
8
spec/factories/webhook_endpoints.rb
Normal file
8
spec/factories/webhook_endpoints.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
FactoryBot.define do
|
||||
factory :webhook_endpoint, class: Webhook::Endpoint do
|
||||
target_url { Faker::Internet.url(scheme: "https") }
|
||||
events { %w[article_created article_updated article_destroyed] }
|
||||
user
|
||||
source { "stackbit" }
|
||||
end
|
||||
end
|
||||
39
spec/models/webhook/endpoint_spec.rb
Normal file
39
spec/models/webhook/endpoint_spec.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe Webhook::Endpoint, type: :model do
|
||||
let!(:endpoint) { create(:webhook_endpoint, user: user, events: %w[article_created article_updated article_destroyed]) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
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.pluck(:id).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.pluck(:id)).to eq([endpoint.id])
|
||||
end
|
||||
|
||||
it "belongs to user" do
|
||||
expect(user.webhook_endpoints.pluck(:id).sort).to eq([endpoint, epoint2].map(&:id).sort)
|
||||
end
|
||||
end
|
||||
end
|
||||
75
spec/requests/api/v0/webhooks_spec.rb
Normal file
75
spec/requests/api/v0/webhooks_spec.rb
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Api::V0::Webhooks", type: :request do
|
||||
let(:user) { create(:user) }
|
||||
let!(:webhook) { create(:webhook_endpoint, user: user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
describe "GET /api/v0/webhooks/:id" do
|
||||
it "returns 200 on success" do
|
||||
get "/api/webhooks/#{webhook.id}"
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it "returns json on success" do
|
||||
get "/api/webhooks/#{webhook.id}"
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["target_url"]).to eq(webhook.target_url)
|
||||
expect(json["user"]["username"]).to eq(user.username)
|
||||
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
|
||||
|
||||
it "creates a webhook" do
|
||||
expect do
|
||||
post "/api/webhooks", 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", 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])
|
||||
end
|
||||
|
||||
it "returns :created and json response on success" do
|
||||
post "/api/webhooks", params: { webhook_endpoint: webhook_params }
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(response.content_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
|
||||
it "deletes the webhook" do
|
||||
expect do
|
||||
delete "/api/webhooks/#{webhook.id}"
|
||||
end.to change(Webhook::Endpoint, :count).by(-1)
|
||||
end
|
||||
|
||||
it "returns 200 on success" do
|
||||
delete "/api/webhooks/#{webhook.id}"
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.content_type).to eq("application/json")
|
||||
json = JSON.parse(response.body)
|
||||
expect(json["success"]).to be true
|
||||
end
|
||||
|
||||
it "doesn't allow to destroy other user webhook" do
|
||||
other_webhook = create(:webhook_endpoint, user: create(:user))
|
||||
expect do
|
||||
delete "/api/webhooks/#{other_webhook.id}"
|
||||
end.not_to change(Webhook::Endpoint, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue