diff --git a/Gemfile b/Gemfile
index 03a19aee1..09d12abb3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,7 @@ gem "redcarpet"
gem "font-awesome-rails"
gem "bootstrap-typeahead-rails"
gem "rails_stdout_logging", "~> 0.0.5", group: [:development, :staging, :production]
+gem "typhoeus"
# Used to store application tokens. This is already a Rails depedency. However
# better safe than sorry...
diff --git a/Gemfile.lock b/Gemfile.lock
index e0c455439..d8b529eb5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -93,7 +93,7 @@ GEM
execjs
coffee-script-source (1.9.1.1)
columnize (0.9.0)
- crack (0.4.2)
+ crack (0.4.3)
safe_yaml (~> 1.0.0)
crono (0.9.0)
activerecord (~> 4.0)
@@ -113,6 +113,8 @@ GEM
excon (>= 0.38.0)
json
erubis (2.7.0)
+ ethon (0.8.1)
+ ffi (>= 1.3.0)
excon (0.49.0)
execjs (2.2.2)
factory_girl (4.5.0)
@@ -355,6 +357,8 @@ GEM
polyglot (~> 0.3)
turbolinks (2.5.3)
coffee-rails
+ typhoeus (1.0.1)
+ ethon (>= 0.8.0)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.7.2)
@@ -441,6 +445,7 @@ DEPENDENCIES
thor
timecop
turbolinks
+ typhoeus
uglifier
vcr
web-console (~> 2.1.3)
diff --git a/app/controllers/api/v2/events_controller.rb b/app/controllers/api/v2/events_controller.rb
index 7de3363c0..0a9a1f9dc 100644
--- a/app/controllers/api/v2/events_controller.rb
+++ b/app/controllers/api/v2/events_controller.rb
@@ -3,7 +3,7 @@ class Api::V2::EventsController < Api::BaseController
# A new notification is coming, register it if valid.
def create
body = JSON.parse(request.body.read)
- Portus::RegistryNotification.process!(body, Repository)
+ Portus::RegistryNotification.process!(body, Repository, Webhook)
head status: :accepted
end
end
diff --git a/app/controllers/webhook_deliveries_controller.rb b/app/controllers/webhook_deliveries_controller.rb
new file mode 100644
index 000000000..e618946da
--- /dev/null
+++ b/app/controllers/webhook_deliveries_controller.rb
@@ -0,0 +1,18 @@
+# WebhookDeliveriesController manages the updates of webhook deliveries.
+class WebhookDeliveriesController < ApplicationController
+ respond_to :html, :js
+
+ after_action :verify_authorized
+
+ # PATCH/PUT /namespaces/1/webhooks/1/deliveries/1
+ def update
+ namespace = Namespace.find(params[:namespace_id])
+ webhook = namespace.webhooks.find(params[:webhook_id])
+ webhook_delivery = webhook.deliveries.find(params[:id])
+
+ authorize webhook_delivery
+
+ webhook_delivery.retrigger
+ render template: "webhooks/retrigger", locals: { webhook_delivery: webhook_delivery }
+ end
+end
diff --git a/app/controllers/webhook_headers_controller.rb b/app/controllers/webhook_headers_controller.rb
index 416589537..8e2b34a26 100644
--- a/app/controllers/webhook_headers_controller.rb
+++ b/app/controllers/webhook_headers_controller.rb
@@ -1,9 +1,9 @@
+# WebhookHeadersController manages the creation/removal of webhook headers.
class WebhookHeadersController < ApplicationController
respond_to :html, :js
before_action :set_namespace
before_action :set_webhook
- before_action :set_webhook_header, only: [:destroy]
after_action :verify_authorized
@@ -23,6 +23,8 @@ def create
# DELETE /namespaces/1/webhooks/1/headers/1
# DELETE /namespaces/1/webhooks/1/headers/1.json
def destroy
+ @webhook_header = @webhook.headers.find(params[:id])
+
authorize @webhook_header
@webhook_header.destroy
@@ -39,10 +41,6 @@ def set_webhook
@webhook = @namespace.webhooks.find(params[:webhook_id])
end
- def set_webhook_header
- @webhook_header = @webhook.headers.find(params[:id])
- end
-
def webhook_header_params
params.require(:webhook_header).permit(:name, :value)
end
diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb
index 47b969180..f6e70c101 100644
--- a/app/controllers/webhooks_controller.rb
+++ b/app/controllers/webhooks_controller.rb
@@ -1,3 +1,5 @@
+# WebhooksController manages the creation/removal/update of webhooks.
+# Also, it manages their state, i.e. enabled/disabled.
class WebhooksController < ApplicationController
respond_to :html, :js
@@ -10,9 +12,8 @@ class WebhooksController < ApplicationController
# GET /namespaces/1/webhooks
# GET /namespaces/1/webhooks.json
def index
- @webhooks = policy_scope(Webhook)
- .where("namespace_id = ?", @namespace.id)
- .page(params[:page])
+ authorize @namespace
+ @webhooks = policy_scope(Webhook).where(namespace: @namespace).page(params[:page])
respond_with(@namespace, @webhooks)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 73c5744d4..4902d34e9 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -39,14 +39,23 @@ class Namespace < ActiveRecord::Base
# namespace, returns two values:
# 1. The namespace where the given repository belongs to.
# 2. The name of the repository itself.
- def self.get_from_name(name)
+ # If a registry is provided, it will query it for the given repository name.
+ def self.get_from_name(name, registry = nil)
if name.include?("/")
namespace, name = name.split("/", 2)
- namespace = Namespace.find_by(name: namespace)
+ if registry.nil?
+ namespace = Namespace.find_by(name: namespace)
+ else
+ namespace = registry.namespaces.find_by(name: namespace)
+ end
else
- namespace = Namespace.find_by(global: true)
+ if registry.nil?
+ namespace = Namespace.find_by(global: true)
+ else
+ namespace = Namespace.find_by(registry: registry, global: true)
+ end
end
- [namespace, name]
+ [namespace, name, registry]
end
# Returns a String containing the cleaned name for this namespace. The
diff --git a/app/models/webhook.rb b/app/models/webhook.rb
index 4bbe7dea7..c492e418c 100644
--- a/app/models/webhook.rb
+++ b/app/models/webhook.rb
@@ -9,7 +9,7 @@
# password :string(255)
# request_method :integer
# content_type :integer
-# enabled :boolean
+# enabled :boolean default("0")
# created_at :datetime not null
# updated_at :datetime not null
#
@@ -18,6 +18,19 @@
# index_webhooks_on_namespace_id (namespace_id)
#
+require "base64"
+require "typhoeus"
+require "securerandom"
+require "json"
+require "uri"
+
+# A Webhook describes a kind of callback to an endpoint defined by an URL.
+# Further parameters are username and password, which are used for basic
+# authentication. The parameters request_method and content_type are limitted
+# to GET and POST, and application/json and application/x-www-form-urlencoded
+# respectively. Webhooks can be enabled or disabled.
+# After a webhook has been triggered with the provided parameters, a
+# WebhookDelivery object is created.
class Webhook < ActiveRecord::Base
include PublicActivity::Common
@@ -26,21 +39,110 @@ class Webhook < ActiveRecord::Base
belongs_to :namespace
- has_many :deliveries, class_name: "WebhookDelivery"
- has_many :headers, class_name: "WebhookHeader"
+ has_many :deliveries, class_name: "WebhookDelivery", dependent: :destroy
+ has_many :headers, class_name: "WebhookHeader", dependent: :destroy
+
+ validates :url, presence: true, url: true
- validates :url, presence: true
+ # default to http if no protocol has been specified. If unspecified, the URL
+ # validator will fail.
+ before_validation do
+ unless url.nil? || url.blank?
+ self.url = "http://#{url}" unless url.start_with?("http://") ||
+ url.start_with?("https://")
+ end
+ end
before_destroy :update_activities!
+ # Handle a push event from the registry. All enabled webhooks of the provided
+ # namespace are triggered in parallel.
+ def self.handle_push_event(event)
+ registry = Registry.find_from_event(event)
+ return if registry.nil?
+
+ namespace, = Namespace.get_from_name(event["target"]["repository"], registry)
+ return if namespace.nil?
+
+ hydra = Typhoeus::Hydra.hydra
+
+ namespace.webhooks.each do |webhook|
+ next unless webhook.enabled
+
+ headers = webhook.process_headers
+
+ args = {
+ method: webhook.request_method,
+ headers: headers,
+ body: JSON.generate(event),
+ timeout: 60,
+ userpwd: webhook.process_auth
+ }
+
+ hydra.queue create_request(webhook, args, headers, event)
+ end
+
+ hydra.run
+ end
+
+ # host returns the host part of the URL. This is useful when wanting a pretty
+ # representation of a webhook.
+ def host
+ _, _, host, = URI.split url
+ host
+ end
+
+ # process_headers returns a hash containing the webhook's headers.
+ def process_headers
+ { "Content-Type" => content_type }.tap do |result|
+ headers.each do |header|
+ result[header.name] = header.value
+ end
+ end
+ end
+
+ # process_auth returns a basic auth string if username and password are provided.
+ def process_auth
+ return if username.empty? || password.empty?
+ "#{username}:#{password}"
+ end
+
private
+ # create_request creates and returns a Request object with the provided arguments.
+ def self.create_request(webhook, args, headers, event)
+ request = Typhoeus::Request.new(webhook.url, args)
+
+ request.on_complete do |response|
+ # prevent uuid clash
+ loop do
+ @uuid = SecureRandom.uuid
+ break if WebhookDelivery.find_by(webhook_id: webhook.id, uuid: @uuid).nil?
+ end
+
+ WebhookDelivery.create(
+ webhook_id: webhook.id,
+ uuid: @uuid,
+ status: response.response_code,
+ request_header: headers.to_s,
+ request_body: JSON.generate(event),
+ response_header: response.response_headers,
+ response_body: response.response_body)
+ end
+
+ request
+ end
+
+ # Provide useful parameters for the "timeline" when a webhook has been
+ # removed.
def update_activities!
+ _, _, host, = URI.split url
PublicActivity::Activity.where(trackable: self).update_all(
parameters: {
namespace_id: namespace.id,
namespace_name: namespace.clean_name,
- webhook_url: url
+ webhook_url: url,
+ webhook_host: host
}
)
end
diff --git a/app/models/webhook_delivery.rb b/app/models/webhook_delivery.rb
index 9e318cded..3a82b5a78 100644
--- a/app/models/webhook_delivery.rb
+++ b/app/models/webhook_delivery.rb
@@ -5,7 +5,7 @@
# id :integer not null, primary key
# webhook_id :integer
# uuid :string(255)
-# status :string(255)
+# status :integer
# request_header :text(65535)
# request_body :text(65535)
# response_header :text(65535)
@@ -19,12 +19,39 @@
# index_webhook_deliveries_on_webhook_id_and_uuid (webhook_id,uuid) UNIQUE
#
+# A WebhookDelivery is created once a webhook has been triggered. They hold
+# information regarding both the request and the response. Webhook deliveries
+# can also be retrigged.
class WebhookDelivery < ActiveRecord::Base
belongs_to :webhook
validates :uuid, uniqueness: { scope: :webhook_id }
+ # success? returns whether or not the request was successful (status 200).
def success?
status == 200
end
+
+ # Retrigger a webhook unconditionally.
+ def retrigger
+ args = {
+ method: webhook.request_method,
+ headers: webhook.process_headers,
+ body: JSON.generate(JSON.load(request_body)),
+ timeout: 60,
+ userpwd: webhook.process_auth
+ }
+
+ request = Typhoeus::Request.new(webhook.url, args)
+
+ request.on_complete do |response|
+ update_attributes status: response.response_code,
+ response_header: response.response_headers,
+ response_body: response.response_body
+ # update `updated_at` field
+ touch
+ end
+
+ request.run
+ end
end
diff --git a/app/models/webhook_header.rb b/app/models/webhook_header.rb
index dc21f3c61..497d78973 100644
--- a/app/models/webhook_header.rb
+++ b/app/models/webhook_header.rb
@@ -15,6 +15,8 @@
# index_webhook_headers_on_webhook_id_and_name (webhook_id,name) UNIQUE
#
+# A WebhookHeader is a key value pair, and describes a HTTP header which is
+# to be included in a webhook request.
class WebhookHeader < ActiveRecord::Base
belongs_to :webhook
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index c725de0ba..12cca1931 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -30,6 +30,12 @@ def push?
namespace.team.contributors.exists?(user.id)
end
+ def index?
+ raise Pundit::NotAuthorizedError, "must be logged in" unless user
+
+ user.admin? || namespace.team.users.exists?(user.id)
+ end
+
alias_method :all?, :push?
alias_method :create?, :push?
alias_method :update?, :push?
diff --git a/app/policies/public_activity/activity_policy.rb b/app/policies/public_activity/activity_policy.rb
index 875d3c59b..33b877911 100644
--- a/app/policies/public_activity/activity_policy.rb
+++ b/app/policies/public_activity/activity_policy.rb
@@ -46,7 +46,48 @@ def resolve
.union_all(namespace_activities)
.union_all(repository_activities)
.union_all(application_token_activities)
+ .union_all(webhook_activities)
.distinct
end
+
+ # webhook_activities returns all webhook activities which are accessibly to
+ # the user.
+ def webhook_activities
+ # Show webhook events only if user is a member of the team controlling
+ # the namespace.
+ # Note: this works only for existing webhooks
+ activities = @scope
+ .joins("INNER JOIN webhooks ON activities.trackable_id = webhooks.id " \
+ "INNER JOIN namespaces ON namespaces.id = webhooks.namespace_id " \
+ "INNER JOIN teams ON teams.id = namespaces.team_id " \
+ "INNER JOIN team_users ON teams.id = team_users.team_id")
+ .where("activities.trackable_type = ? AND team_users.user_id = ?",
+ "Webhook", user.id)
+
+ # Convert relation to array since we want to add single objects, and objects
+ # cannot be added to relations.
+ activities = activities.to_a
+
+ # Get all namespaces the user has access to.
+ user_namespaces = Namespace
+ .where(team_id: TeamUser.where(user_id: user.id).pluck(:team_id))
+ .pluck(:id)
+
+ # Go through all webhooks and add those to the array whose namespace_id is
+ # included in the user's accessible namespaces. This step is needed, as
+ # there is no (easy) way to match these webhooks with their corresponding
+ # namespace.
+ @scope
+ .where("activities.trackable_type = ?", "Webhook").distinct.find_each do |webhook|
+ unless webhook.parameters.empty?
+ if user_namespaces.include? webhook.parameters[:namespace_id]
+ activities << webhook
+ end
+ end
+ end
+
+ # "convert" array back to relation in order to use `union_all`.
+ @scope.where(id: activities.map(&:id))
+ end
end
end
diff --git a/app/policies/webhook_delivery_policy.rb b/app/policies/webhook_delivery_policy.rb
new file mode 100644
index 000000000..917a4f86e
--- /dev/null
+++ b/app/policies/webhook_delivery_policy.rb
@@ -0,0 +1,13 @@
+class WebhookDeliveryPolicy < WebhookPolicy
+ attr_reader :webhook_delivery
+
+ def initialize(user, webhook_delivery)
+ raise Pundit::NotAuthorizedError, "must be logged in" unless user
+
+ @user = user
+ @webhook_delivery = webhook_delivery
+
+ @webhook = webhook_delivery.webhook
+ @namespace = @webhook.namespace
+ end
+end
diff --git a/app/policies/webhook_policy.rb b/app/policies/webhook_policy.rb
index 5f0e5f13a..185a707b8 100644
--- a/app/policies/webhook_policy.rb
+++ b/app/policies/webhook_policy.rb
@@ -10,12 +10,22 @@ def initialize(user, webhook)
@namespace = webhook.namespace
end
- def toggle_enabled?
+ def create?
raise Pundit::NotAuthorizedError, "must be logged in" unless user
+
+ # Only admins and owners have WRITE access
user.admin? || namespace.team.owners.exists?(user.id)
end
- alias_method :destroy?, :push?
+ def show?
+ raise Pundit::NotAuthorizedError, "must be logged in" unless user
+
+ user.admin? || namespace.team.users.exists?(user.id)
+ end
+
+ alias_method :destroy?, :create?
+ alias_method :toggle_enabled?, :create?
+ alias_method :update?, :create?
class Scope
attr_reader :user, :scope
@@ -35,11 +45,9 @@ def resolve
"(namespaces.public = :public OR team_users.user_id = :user_id) AND " \
"namespaces.global = :global AND namespaces.name != :username",
public: true, user_id: user.id, global: false, username: user.username)
- .pluk(:id)
+ .pluck(:id)
- scope
- .includes(:headers, :deliveries)
- .where("namespace_id = ?", namespaces)
+ scope.includes(:headers, :deliveries).where(namespace: namespaces)
end
end
end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
new file mode 100644
index 000000000..ccfe6695a
--- /dev/null
+++ b/app/validators/url_validator.rb
@@ -0,0 +1,13 @@
+require "uri"
+
+# Validates URLs
+class UrlValidator < ActiveModel::EachValidator
+ # Validator for the url.
+ def validate_each(record, attribute, value)
+ uri = URI.parse(value)
+ return if uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
+ record.errors[attribute] << "is not a valid URL"
+ rescue URI::InvalidURIError
+ record.errors[attribute] << "is not a valid URL"
+ end
+end
diff --git a/app/views/admin/namespaces/index.html.slim b/app/views/admin/namespaces/index.html.slim
index f99bc9c8a..70c164c90 100644
--- a/app/views/admin/namespaces/index.html.slim
+++ b/app/views/admin/namespaces/index.html.slim
@@ -6,13 +6,15 @@
.table-responsive
table.table.table-stripped.table-hover
col.col-40
- col.col-30
+ col.col-15
+ col.col-15
col.col-20
col.col-10
thead
tr
th Name
th Repositories
+ th Webhooks
th Created At
th Public
tbody
@@ -27,13 +29,15 @@
.table-responsive
table.table.table-stripped.table-hover
col.col-40
- col.col-30
+ col.col-15
+ col.col-15
col.col-20
col.col-10
thead
tr
th Name
th Repositories
+ th Webhooks
th Created At
th Public
tbody
diff --git a/app/views/namespaces/_namespace.html.slim b/app/views/namespaces/_namespace.html.slim
index c82b7c29f..ea332b974 100644
--- a/app/views/namespaces/_namespace.html.slim
+++ b/app/views/namespaces/_namespace.html.slim
@@ -1,6 +1,10 @@
tr id="namespace_#{namespace.id}"
td= link_to namespace.clean_name, namespace
td= namespace.repositories.count
+ - if current_user.admin? || namespace.team.users.include?(current_user)
+ td= link_to namespace.webhooks.count, namespace_webhooks_path(namespace)
+ - else
+ td= namespace.webhooks.count
td= time_tag namespace.created_at
td
- if can_manage_namespace?(namespace) && !namespace.global
diff --git a/app/views/namespaces/index.html.slim b/app/views/namespaces/index.html.slim
index 8aa2881e2..e6f64e9ad 100644
--- a/app/views/namespaces/index.html.slim
+++ b/app/views/namespaces/index.html.slim
@@ -11,13 +11,15 @@ span
.table-responsive
table.table.table-stripped.table-hover
col.col-40
- col.col-30
+ col.col-15
+ col.col-15
col.col-20
col.col-10
thead
tr
th Name
th Repositories
+ th Webhooks
th Created
th Public
tbody
@@ -57,13 +59,15 @@ span
.table-responsive
table.table.table-stripped.table-hover
col.col-40
- col.col-30
+ col.col-15
+ col.col-15
col.col-20
col.col-10
thead
tr
th Name
th Repositories
+ th Webhooks
th Created At
th Public
tbody#accessible-namespaces
diff --git a/app/views/namespaces/show.html.slim b/app/views/namespaces/show.html.slim
index bd48b39b8..88c4a14d8 100644
--- a/app/views/namespaces/show.html.slim
+++ b/app/views/namespaces/show.html.slim
@@ -101,7 +101,14 @@
tabindex="0" data-html="true"]
i.fa.fa-eye
' Viewer
-
+ - if current_user.admin? || @namespace.team.users.include?(current_user)
+ .pull-right
+ a.btn.btn-link.btn-xs.btn-show-webhooks[
+ value="#{@namespace.id}" class="button_namespace_description"
+ href=url_for(namespace_webhooks_path(@namespace))
+ ]
+ i.fa.fa-eye.fa-lg
+ | Show webhooks
.panel-body
.table-responsive
table.table.table-stripped.table-hover
diff --git a/app/views/public_activity/webhook/_create.html.slim b/app/views/public_activity/webhook/_create.html.slim
index f272b660f..902a9ed34 100644
--- a/app/views/public_activity/webhook/_create.html.slim
+++ b/app/views/public_activity/webhook/_create.html.slim
@@ -7,10 +7,16 @@ li
.description
h6
strong
- = "#{activity.owner.username} created the "
- = link_to activity.trackable.url, [activity.trackable.namespace, activity.trackable]
- = " webhook under the "
- = link_to activity.trackable.namespace.name, activity.trackable.namespace
+ = "#{activity.owner.username} created webhook "
+ - if activity.trackable
+ = link_to activity.trackable.host, [activity.trackable.namespace, activity.trackable]
+ - else
+ = activity.parameters[:webhook_host]
+ = " under the "
+ - if activity.trackable
+ = link_to activity.trackable.namespace.name, activity.trackable.namespace
+ - else
+ = link_to activity.parameters[:namespace_name], controller: "namespaces", id: activity.parameters[:namespace_id]
= " namespace"
small
i.fa.fa-clock-o
diff --git a/app/views/public_activity/webhook/_destroy.html.slim b/app/views/public_activity/webhook/_destroy.html.slim
index 281ae0702..015d71e8f 100644
--- a/app/views/public_activity/webhook/_destroy.html.slim
+++ b/app/views/public_activity/webhook/_destroy.html.slim
@@ -7,10 +7,10 @@ li
.description
h6
strong
- = "#{activity.owner.username} removed the "
- = activity.trackable.url
- = " webhook from the "
- = activity.trackable.namespace.name
+ = "#{activity.owner.username} removed webhook "
+ = activity.parameters[:webhook_host]
+ = " from the "
+ = link_to activity.parameters[:namespace_name], controller: "namespaces", id: activity.parameters[:namespace_id]
= " namespace"
small
i.fa.fa-clock-o
diff --git a/app/views/public_activity/webhook/_disabled.html.slim b/app/views/public_activity/webhook/_disabled.html.slim
index ea9cea4b2..a63e9733a 100644
--- a/app/views/public_activity/webhook/_disabled.html.slim
+++ b/app/views/public_activity/webhook/_disabled.html.slim
@@ -8,7 +8,10 @@ li
h6
strong
= "#{activity.owner.username} set the "
- = link_to activity.trackable.url, [activity.trackable.namespace, activity.trackable]
+ - if activity.trackable.nil?
+ = activity.parameters[:webhook_host]
+ - else
+ = link_to activity.trackable.host, [activity.trackable.namespace, activity.trackable]
= " webhook as disabled"
small
i.fa.fa-clock-o
diff --git a/app/views/public_activity/webhook/_enabled.html.slim b/app/views/public_activity/webhook/_enabled.html.slim
index b6bb1483b..868b5667a 100644
--- a/app/views/public_activity/webhook/_enabled.html.slim
+++ b/app/views/public_activity/webhook/_enabled.html.slim
@@ -8,7 +8,10 @@ li
h6
strong
= "#{activity.owner.username} set the "
- = link_to activity.trackable.url, [activity.trackable.namespace, activity.trackable]
+ - if activity.trackable.nil?
+ = activity.parameters[:webhook_host]
+ - else
+ = link_to activity.trackable.host, [activity.trackable.namespace, activity.trackable]
= " webhook as enabled"
small
i.fa.fa-clock-o
diff --git a/app/views/public_activity/webhook/_update.html.slim b/app/views/public_activity/webhook/_update.html.slim
new file mode 100644
index 000000000..adad31a08
--- /dev/null
+++ b/app/views/public_activity/webhook/_update.html.slim
@@ -0,0 +1,23 @@
+li
+ .activitie-container
+ .activity-type.update-webhook
+ i.fa.fa-server
+ .user-image
+ = user_image_tag(activity.owner.email)
+ .description
+ h6
+ strong
+ = "#{activity.owner.username} updated webhook "
+ - if activity.trackable
+ = link_to activity.trackable.host, [activity.trackable.namespace, activity.trackable]
+ - else
+ = activity.parameters[:webhook_host]
+ = " under the "
+ - if activity.trackable
+ = link_to activity.trackable.namespace.name, activity.trackable.namespace
+ - else
+ = link_to activity.parameters[:namespace_name], controller: "namespaces", id: activity.parameters[:namespace_id]
+ = " namespace"
+ small
+ i.fa.fa-clock-o
+ = activity_time_tag activity.created_at
diff --git a/app/views/teams/show.html.slim b/app/views/teams/show.html.slim
index 7a3b20d31..26525aba5 100644
--- a/app/views/teams/show.html.slim
+++ b/app/views/teams/show.html.slim
@@ -156,13 +156,15 @@
table.table.table-striped.table-hover
colgroup
col.col-50
- col.col-30
+ col.col-15
+ col.col-15
col.col-20
col.col-10
thead
tr
th Namespace
th Repositories
+ th Webhooks
th Created At
th Public
tbody#namespaces
diff --git a/app/views/webhook_headers/_webhook_header.html.slim b/app/views/webhook_headers/_webhook_header.html.slim
index 05a5153af..dc5a9e942 100644
--- a/app/views/webhook_headers/_webhook_header.html.slim
+++ b/app/views/webhook_headers/_webhook_header.html.slim
@@ -1,14 +1,18 @@
tr id="webhook_header_#{webhook_header.id}"
td= webhook_header.name
- td= webhook_header.value
- td
- a[class="btn btn-default"
- data-placement="left"
- data-toggle="popover"
- data-title="Please confirm"
- data-content='
Are you sure you want to remove this webhook header?
No Yes'
- data-html="true"
- tabindex="0"
- role="button"
- ]
- i.fa.fa-trash.fa-lg
+ - if can_manage_namespace?(@namespace)
+ td= webhook_header.value
+ - else
+ td= '*' * webhook_header.value.length
+ - if can_manage_namespace?(@namespace)
+ td
+ a[class="btn btn-default"
+ data-placement="left"
+ data-toggle="popover"
+ data-title="Please confirm"
+ data-content='Are you sure you want to remove this webhook header?
No Yes'
+ data-html="true"
+ tabindex="0"
+ role="button"
+ ]
+ i.fa.fa-trash.fa-lg
diff --git a/app/views/webhook_headers/create.js.erb b/app/views/webhook_headers/create.js.erb
index 8f8db631f..764fb58e7 100644
--- a/app/views/webhook_headers/create.js.erb
+++ b/app/views/webhook_headers/create.js.erb
@@ -2,7 +2,7 @@
$('#alert p').html("<%= escape_javascript(@webhook_header.errors.full_messages.join('
')) %>");
$('#alert').fadeIn();
<% else %>
- $('#alert p').html("New header created");
+ $('#alert p').html("Header '<%= @webhook_header.name %>' has been created successfully");
$('#alert').fadeIn();
$('#add_webhook_header_form').fadeOut();
diff --git a/app/views/webhook_headers/destroy.js.erb b/app/views/webhook_headers/destroy.js.erb
index e785d1e50..1c8cdd256 100644
--- a/app/views/webhook_headers/destroy.js.erb
+++ b/app/views/webhook_headers/destroy.js.erb
@@ -3,6 +3,6 @@
$('#alert').fadeIn();
<% else %>
$("#webhook_header_<%= @webhook_header.id %>").remove();
- $('#alert p').html("Header removed from the webhook");
+ $('#alert p').html("Header '<%= @webhook_header.name %>' has been removed successfully");
$('#alert').fadeIn();
<% end %>
diff --git a/app/views/webhooks/_webhook.html.slim b/app/views/webhooks/_webhook.html.slim
index fb3eb2409..c1bf6828b 100644
--- a/app/views/webhooks/_webhook.html.slim
+++ b/app/views/webhooks/_webhook.html.slim
@@ -3,23 +3,30 @@ tr id="webhook_#{webhook.id}"
td= webhook.request_method
td= webhook.content_type
td
- a.btn.btn-default[data-remote="true"
- data-method="put"
- rel="nofollow"
- href=url_for(toggle_enabled_namespace_webhook_path(namespace, webhook))
- ]
+ - if can_manage_namespace?(@namespace)
+ a.btn.btn-default[data-remote="true"
+ data-method="put"
+ rel="nofollow"
+ href=url_for(toggle_enabled_namespace_webhook_path(namespace, webhook))
+ ]
+ - if webhook.enabled?
+ i.fa.fa-lg class="fa-toggle-on"
+ - else
+ i.fa.fa-lg class="fa-toggle-off" title="Click so this webhook gets enabled"
+ - else
- if webhook.enabled?
i.fa.fa-lg class="fa-toggle-on"
- else
- i.fa.fa-lg class="fa-toggle-off" title="Click so this webhook gets enabled"
- td
- a[class="btn btn-default"
- data-placement="left"
- data-toggle="popover"
- data-title="Please confirm"
- data-content='Are you sure you want to remove this webhook?
No Yes'
- data-html="true"
- tabindex="0"
- role="button"
- ]
- i.fa.fa-trash.fa-lg
+ i.fa.fa-lg class="fa-toggle-off"
+ - if can_manage_namespace?(@namespace)
+ td
+ a[class="btn btn-default"
+ data-placement="left"
+ data-toggle="popover"
+ data-title="Please confirm"
+ data-content='Are you sure you want to remove this webhook?
No Yes'
+ data-html="true"
+ tabindex="0"
+ role="button"
+ ]
+ i.fa.fa-trash.fa-lg
diff --git a/app/views/webhooks/create.js.erb b/app/views/webhooks/create.js.erb
index 980bdeed2..0ac1de36a 100644
--- a/app/views/webhooks/create.js.erb
+++ b/app/views/webhooks/create.js.erb
@@ -2,12 +2,12 @@
$('#alert p').html("<%= escape_javascript(@webhook.errors.full_messages.join('
')) %>");
$('#alert').fadeIn();
<% else %>
- $('#alert p').html("New webhook created");
+ $('#alert p').html("Webhook '<%= @webhook.host %>' has been created successfully");
$('#alert').fadeIn();
$('#add_webhook_form').fadeOut();
- $('#add_webhook_btn i').addClass("fa-chevron-down")
- $('#add_webhook_btn i').removeClass("fa-chevron-up")
+ $('#add_webhook_btn i').addClass("fa-plus-circle")
+ $('#add_webhook_btn i').removeClass("fa-minus-circle")
if ($('#webhooks').length > 0) {
$("<%= escape_javascript(render partial: "webhooks/webhook", locals: {namespace: @namespace,webhook: @webhook}) %>").appendTo("#webhooks");
diff --git a/app/views/webhooks/destroy.js.erb b/app/views/webhooks/destroy.js.erb
index 8a4fc474d..90038abc7 100644
--- a/app/views/webhooks/destroy.js.erb
+++ b/app/views/webhooks/destroy.js.erb
@@ -3,6 +3,6 @@
$('#alert').fadeIn();
<% else %>
$("#webhook_<%= @webhook.id %>").remove();
- $('#alert p').html("Webhook removed from the namespace");
+ $('#alert p').html("Webhook '<%= @webhook.host %>' has been removed successfully");
$('#alert').fadeIn();
<% end %>
diff --git a/app/views/webhooks/index.html.slim b/app/views/webhooks/index.html.slim
index 4e995b8e3..cd705ee9d 100644
--- a/app/views/webhooks/index.html.slim
+++ b/app/views/webhooks/index.html.slim
@@ -24,29 +24,45 @@
.col-md-offset-2.col-md-7
= f.submit('Create', class: 'btn btn-primary')
+span
+ a[data-placement="right" data-toggle="popover" data-container="span" data-content="A webhook is an HTTP callback which is triggered after an event, e.g. a push event, occurs." data-original-title="What's this?" tabindex="0"]
+ i.fa.fa-info-circle
+ | What's this?
.panel.panel-default
.panel-heading
h5
- ' Webhooks
- .pull-right
- a#add_webhook_btn.btn.btn-xs.btn-link.js-toggle-button[role="button"]
- i.fa.fa-plus-circle
- | Create new webhook
+ ' Webhooks for
+ - if @namespace.name == "portus_global_namespace_1"
+ strong
+ = link_to " global namespace", @namespace
+ - else
+ ' namespace
+ strong
+ = link_to @namespace.name, @namespace
+ - if can_manage_namespace?(@namespace)
+ .pull-right
+ a#add_webhook_btn.btn.btn-xs.btn-link.js-toggle-button[role="button"]
+ i.fa.fa-plus-circle
+ | Create new webhook
.panel-body
.table-responsive
table.table.table-stripped.table-hover
col.col-40
col.col-20
col.col-20
- col.col-10
- col.col-10
+ - if can_manage_namespace?(@namespace)
+ col.col-10
+ col.col-10
+ -else
+ col.col-20
thead
tr
th URL
th Request method
th Content type
th Enabled
- th Remove
+ - if can_manage_namespace?(@namespace)
+ th Remove
tbody#webhooks
- @webhooks.each do |webhook|
= render partial: 'webhooks/webhook', locals: {namespace: @namespace, webhook: webhook}
diff --git a/app/views/webhooks/retrigger.js.erb b/app/views/webhooks/retrigger.js.erb
new file mode 100644
index 000000000..ad20ba346
--- /dev/null
+++ b/app/views/webhooks/retrigger.js.erb
@@ -0,0 +1,13 @@
+var ns = $('#webhook_delivery_<%= webhook_delivery.id %> td i').first();
+
+<% if webhook_delivery.success? %>
+ ns.addClass("fa-check");
+ ns.addClass("text-success");
+ ns.removeClass("fa-close");
+ ns.removeClass("text-danger");
+<% else %>
+ ns.addClass("fa-close");
+ ns.addClass("text-danger");
+ ns.removeClass("fa-check");
+ ns.removeClass("text-success");
+<% end %>
diff --git a/app/views/webhooks/show.html.slim b/app/views/webhooks/show.html.slim
index a55d0c4e0..9a57ba3ac 100644
--- a/app/views/webhooks/show.html.slim
+++ b/app/views/webhooks/show.html.slim
@@ -1,89 +1,103 @@
.panel-group
- .collapse id="update_webhook_#{@webhook.id}"
- = form_for [@namespace, @webhook], remote: true, html: {class: 'form-horizontal', role: 'form'} do |f|
- .panel.panel-default
- .panel-heading
- .input-group
- = f.text_field(:url, class: 'form-control', required: true, placeholder: html_escape(@webhook.url), input_html: { tabindex: 1 })
- .input-group-btn
- button.btn.btn-link.btn-xs.btn-edit-webhook[
- value="#{@webhook.id}"
- type="button"
- class="button_edit_webhook"]
- i.fa.fa-close
- | Edit webhook
- .panel-body
- .form-group
- = f.label :request_method, {class: 'control-label col-md-2'}
- .col-md-7
- = f.select(:request_method, Webhook.request_methods.keys, {}, {class: 'form-control'})
- .form-group
- = f.label :content_type, {class: 'control-label col-md-2'}
- .col-md-7
- = f.select(:content_type, Webhook.content_types.keys, {}, {class: 'form-control'})
- .form-group
- = f.label :username, {class: 'control-label col-md-2'}
- .col-md-7
- = f.text_field(:username, class: 'form-control', required: false, placeholder: "Username for authentication")
- .form-group
- = f.label :password, {class: 'control-label col-md-2'}
- .col-md-7
- = f.text_field(:password, class: 'form-control', required: false, placeholder: "Password for authentication")
- .form-group
- .col-md-offset-2.col-md-7
- = f.submit('Save', class: 'btn btn-primary')
- .errors
+ - if can_manage_namespace?(@namespace)
+ .collapse id="update_webhook_#{@webhook.id}"
+ = form_for [@namespace, @webhook], remote: true, html: {class: 'form-horizontal', role: 'form'} do |f|
+ .panel.panel-default
+ .panel-heading
+ .input-group
+ = f.text_field(:url, class: 'form-control', required: true, placeholder: html_escape(@webhook.url), input_html: { tabindex: 1 })
+ .input-group-btn
+ button.btn.btn-link.btn-xs.btn-edit-webhook[
+ value="#{@webhook.id}"
+ type="button"
+ class="button_edit_webhook"]
+ i.fa.fa-close
+ | Edit webhook
+ .panel-body
+ .form-group
+ = f.label :request_method, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.select(:request_method, Webhook.request_methods.keys, {}, {class: 'form-control'})
+ .form-group
+ = f.label :content_type, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.select(:content_type, Webhook.content_types.keys, {}, {class: 'form-control'})
+ .form-group
+ = f.label :username, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.text_field(:username, class: 'form-control', required: false, placeholder: "Username for authentication")
+ .form-group
+ = f.label :password, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.text_field(:password, class: 'form-control', required: false, placeholder: "Password for authentication")
+ .form-group
+ .col-md-offset-2.col-md-7
+ = f.submit('Save', class: 'btn btn-primary')
+ .errors
.panel.panel-default.webhook_information
.panel-heading
h5
strong
- '#{@webhook.url}
+ '#{@webhook.host}
' webhook
- .pull-right
- button.btn.btn-link.btn-xs.btn-edit-webhook[
- value="#{@webhook.id}"
- class="button_edit_webhook"]
- i.fa.fa-pencil
- | Edit webhook
+ small
+ a[data-placement="right" data-toggle="popover" data-container=".panel-heading" data-content="Request method: URL endpoint where the HTTP request is sent to.
Content type: Description of the webhook request content.
Username: Username used for basic HTTP auth.
Password: Password used for basic HTTP auth." data-original-title="What's this?" tabindex="0" data-html="true"]
+ i.fa.fa-info-circle
+ - if can_manage_namespace?(@namespace)
+ .pull-right
+ button.btn.btn-link.btn-xs.btn-edit-webhook[
+ value="#{@webhook.id}"
+ class="button_edit_webhook"]
+ i.fa.fa-pencil
+ | Edit webhook
.panel-body
= render partial: "detail", locals: {webhook: @webhook}
-#add_webhook_header_form.collapse
- = form_for :webhook_header, url: namespace_webhook_headers_path(@namespace, @webhook), remote: true, html: {id: 'new-header-form', class: 'form-horizontal', role: 'form'} do |f|
- .form-group
- = f.label :name, {class: 'control-label col-md-2'}
- .col-md-7
- = f.text_field(:name, class: 'form-control', required: true, placeholder: "Name")
- .form-group
- = f.label :value, {class: 'control-label col-md-2'}
- .col-md-7
- = f.text_field(:value, class: 'form-control', required: true, placeholder: "Value")
- .form-group
- .col-md-offset-2.col-md-7
- = f.submit('Create', class: 'btn btn-primary')
- .errors
+- if can_manage_namespace?(@namespace)
+ #add_webhook_header_form.collapse
+ = form_for :webhook_header, url: namespace_webhook_headers_path(@namespace, @webhook), remote: true, html: {id: 'new-header-form', class: 'form-horizontal', role: 'form'} do |f|
+ .form-group
+ = f.label :name, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.text_field(:name, class: 'form-control', required: true, placeholder: "Name")
+ .form-group
+ = f.label :value, {class: 'control-label col-md-2'}
+ .col-md-7
+ = f.text_field(:value, class: 'form-control', required: true, placeholder: "Value")
+ .form-group
+ .col-md-offset-2.col-md-7
+ = f.submit('Create', class: 'btn btn-primary')
+ .errors
.panel.panel-default
.panel-heading
h5
' Headers
- .pull-right
- a#add_webhook_header_btn.btn.btn-xs.btn-link.js-toggle-button[role="button"]
- i.fa.fa-plus-circle
- | Create new header
+ small
+ a[data-placement="right" data-toggle="popover" data-container=".panel-heading" data-content="A header is a HTTP header, i.e. is a key-value pair which is included in the HTTP request." data-original-title="What's this?" tabindex="0"]
+ i.fa.fa-info-circle
+ - if can_manage_namespace?(@namespace)
+ .pull-right
+ a#add_webhook_header_btn.btn.btn-xs.btn-link.js-toggle-button[role="button"]
+ i.fa.fa-plus-circle
+ | Create new header
.panel-body
.table-responsive
table.table.table-stripped.table-hover
colgroup
col.col-20
- col.col-70
- col.col-10
+ - if can_manage_namespace?(@namespace)
+ col.col-70
+ col.col-10
+ - else
+ col.col-80
thead
tr
th Name
th Value
- th Remove
+ - if can_manage_namespace?(@namespace)
+ th Remove
tbody#webhook_headers
- @webhook.headers.each do |header|
= render partial: 'webhook_headers/webhook_header', locals: {namespace: @namespace, webhook: @webhook, webhook_header: header}
@@ -92,28 +106,49 @@
.panel-heading
h5
' Deliveries
+ small
+ a[data-placement="right" data-toggle="popover" data-container=".panel-heading" data-content="A delivery is created once a webhook has been triggered. They are not re-created but updated after retriggering." data-original-title="What's this?" tabindex="0"]
+ i.fa.fa-info-circle
.panel-body
.table-responsive
table.table.table-stripped.table-hover
colgroup
- col.col-90
- col.col-10
+ - if can_manage_namespace?(@namespace)
+ col.col-90
+ col.col-10
+ - else
+ col.col-100
thead
tr
th UUID
- th Retrigger
+ - if can_manage_namespace?(@namespace)
+ th Retrigger
tbody#webhook_deliveries
- @deliveries.each do |delivery|
- tr
+ tr[id="webhook_delivery_#{delivery.id}"]
td
- if delivery.success?
i.fa.fa-check.fa-lg.text-success
- else
i.fa.fa-close.fa-lg.text-danger
= delivery.uuid
- td
- button.btn.btn-default.btn-retrigger-webhook[
- value="#{delivery.uuid}"
- class="details"]
- i.fa.fa-refresh.fa-lg
+ - if can_manage_namespace?(@namespace)
+ td
+ - if delivery.webhook.enabled
+ a.btn.btn-default.btn-retrigger-webhook[data-remote="true"
+ data-method="put"
+ rel="nofollow"
+ href=url_for(namespace_webhook_delivery_path(@namespace, @webhook, delivery))
+ ]
+ i.fa.fa-refresh.fa-lg
+ - else
+ a.btn.btn-default.btn-retrigger-webhook[data-placement="left"
+ data-toggle="popover"
+ data-title="Please confirm"
+ data-content='This webhook is disabled. Are you sure you want to retrigger it?
No Yes'
+ data-html="true"
+ tabindex="0"
+ role="button"
+ ]
+ i.fa.fa-refresh.fa-lg
.panel-footer= paginate(@deliveries)
diff --git a/app/views/webhooks/toggle_enabled.js.erb b/app/views/webhooks/toggle_enabled.js.erb
index 11a52dd62..4a358c322 100644
--- a/app/views/webhooks/toggle_enabled.js.erb
+++ b/app/views/webhooks/toggle_enabled.js.erb
@@ -1,11 +1,11 @@
-var ns = $('#webhook_<%= webhook.id %> td a i');
+var ns = $('#webhook_<%= webhook.id %> td a i').first();
<% if webhook.enabled? %>
- ns.removeAttr("title");
+ ns.prop("title", "This webhook is enabled and will be triggered");
ns.addClass("fa-toggle-on");
ns.removeClass("fa-toggle-off");
<% else %>
- ns.prop("title", "This webhook is enabled and will be trieggered");
+ ns.removeAttr("title");
ns.addClass("fa-toggle-off");
ns.removeClass("fa-toggle-on");
<% end %>
diff --git a/app/views/webhooks/update.js.erb b/app/views/webhooks/update.js.erb
index 6541657d2..22c1ec8d3 100644
--- a/app/views/webhooks/update.js.erb
+++ b/app/views/webhooks/update.js.erb
@@ -1,5 +1,5 @@
<% if @webhook.errors.any? %>
- $('#alert p').html("<%= escape_javascript(@webhook.errors.full_messages.join('
')) %>");
+ $('#alert p').html("<%= escape_javascript(raw(@webhook.errors.full_messages.join('
'))) %>");
$('#alert').fadeIn();
<% else %>
$('#update_webhook_<%= @webhook.id %>').toggle()
@@ -10,4 +10,7 @@
el.addClass('fa-pencil');
$('.webhook_information .panel-body').html("<%= escape_javascript(render partial: "detail", locals: {webhook: @webhook}) %>");
+ $('#alert p').html("Webhook '<%= @webhook.host %>' has been updated successfully");
+ $('#alert').fadeIn();
+ $('.webhook_information .panel-heading h5 strong').text("<%= @webhook.host %> ")
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index 2aed939d7..bab1f3d42 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -12,6 +12,7 @@
put "toggle_public", on: :member
resources :webhooks do
resources :headers, only: [:create, :destroy], controller: :webhook_headers
+ resources :deliveries, only: [:update], controller: :webhook_deliveries
member do
put "toggle_enabled"
end
diff --git a/db/migrate/20160411150441_create_webhooks.rb b/db/migrate/20160411150441_create_webhooks.rb
index c4b865510..eaa81fba2 100644
--- a/db/migrate/20160411150441_create_webhooks.rb
+++ b/db/migrate/20160411150441_create_webhooks.rb
@@ -7,7 +7,7 @@ def change
t.string :password
t.integer :request_method
t.integer :content_type
- t.boolean :enabled
+ t.boolean :enabled, default: false
t.timestamps null: false
end
diff --git a/db/migrate/20160411150745_create_webhook_deliveries.rb b/db/migrate/20160411150745_create_webhook_deliveries.rb
index a2409545b..535cbba64 100644
--- a/db/migrate/20160411150745_create_webhook_deliveries.rb
+++ b/db/migrate/20160411150745_create_webhook_deliveries.rb
@@ -3,7 +3,7 @@ def change
create_table :webhook_deliveries do |t|
t.references :webhook, index: true, foreign_key: true
t.string :uuid
- t.string :status
+ t.integer :status
t.text :request_header
t.text :request_body
t.text :response_header
diff --git a/db/schema.rb b/db/schema.rb
index afd09e33e..3ba51056f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -175,7 +175,7 @@
create_table "webhook_deliveries", force: :cascade do |t|
t.integer "webhook_id", limit: 4
t.string "uuid", limit: 255
- t.string "status", limit: 255
+ t.integer "status", limit: 4
t.text "request_header", limit: 65535
t.text "request_body", limit: 65535
t.text "response_header", limit: 65535
@@ -205,9 +205,9 @@
t.string "password", limit: 255
t.integer "request_method", limit: 4
t.integer "content_type", limit: 4
- t.boolean "enabled", limit: 1
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.boolean "enabled", default: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "webhooks", ["namespace_id"], name: "index_webhooks_on_namespace_id", using: :btree
diff --git a/lib/portus/registry_notification.rb b/lib/portus/registry_notification.rb
index 124d36953..2aa0b61b6 100644
--- a/lib/portus/registry_notification.rb
+++ b/lib/portus/registry_notification.rb
@@ -5,11 +5,11 @@ class RegistryNotification
# An array with the events that a handler has to support.
HANDLED_EVENTS = ["push", "delete"].freeze
- # Processes the notification data with the given handler. The data is the
+ # Processes the notification data with the given handlers. The data is the
# parsed JSON body as given by the registry. A handler is a class that can
# call the `handle_#{event}_event` method. This method receives an `event`
# object, which is the event object as given by the registry.
- def self.process!(data, handler)
+ def self.process!(data, *handlers)
data["events"].each do |event|
Rails.logger.debug "Incoming event:\n#{JSON.pretty_generate(event)}"
next unless relevant?(event)
@@ -18,7 +18,7 @@ def self.process!(data, handler)
next unless HANDLED_EVENTS.include?(action)
Rails.logger.info "Handling '#{action}' event:\n#{JSON.pretty_generate(event)}"
- handler.send("handle_#{action}_event".to_sym, event)
+ handlers.each { |handler| handler.send("handle_#{action}_event".to_sym, event) }
end
end
diff --git a/spec/controllers/webhook_headers_controller_spec.rb b/spec/controllers/webhook_headers_controller_spec.rb
index 08567a3fc..8cff363a0 100644
--- a/spec/controllers/webhook_headers_controller_spec.rb
+++ b/spec/controllers/webhook_headers_controller_spec.rb
@@ -1,5 +1,77 @@
-require 'rails_helper'
+require "rails_helper"
RSpec.describe WebhookHeadersController, type: :controller do
+ let(:valid_session) { {} }
+ let!(:registry) { create(:registry) }
+ let(:user) { create(:user) }
+ let(:viewer) { create(:user) }
+ let(:contributor) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:team) do
+ create(:team,
+ owners: [owner],
+ viewers: [user, viewer],
+ contributors: [contributor])
+ end
+ let(:namespace) do
+ create(
+ :namespace,
+ team: team,
+ description: "short test description",
+ registry: registry)
+ end
+ let(:webhook) { create(:webhook, namespace: namespace) }
+ describe "POST #create" do
+ context "as a namespace owner" do
+ let(:post_params) do
+ {
+ webhook_id: webhook.id,
+ namespace_id: namespace.id,
+ webhook_header: { name: "foo", value: "bar" },
+ format: :js
+ }
+ end
+
+ it "creates a webhook header" do
+ sign_in owner
+ post :create, post_params
+ expect(response.status).to eq(200)
+ end
+
+ it "disallows creating multiple headers with the same name" do
+ sign_in owner
+ post :create, post_params
+ post :create, post_params
+ expect(response.status).to eq(422)
+ end
+ end
+ end
+
+ describe "DELETE #destroy" do
+ let(:webhook_header) do
+ create(:webhook_header, webhook: webhook, name: "foo", value: "bar")
+ end
+
+ let(:post_params) do
+ {
+ id: webhook_header.id,
+ webhook_id: webhook.id,
+ namespace_id: namespace.id,
+ format: :js
+ }
+ end
+
+ it "allows owner to delete webhook" do
+ sign_in owner
+ delete :destroy, post_params
+ expect(response.status).to eq(200)
+ end
+
+ it "disallows user to delete webhook" do
+ sign_in user
+ delete :destroy, post_params
+ expect(response.status).to eq(401)
+ end
+ end
end
diff --git a/spec/controllers/webhooks_controller_spec.rb b/spec/controllers/webhooks_controller_spec.rb
index 56f9119b5..935195115 100644
--- a/spec/controllers/webhooks_controller_spec.rb
+++ b/spec/controllers/webhooks_controller_spec.rb
@@ -1,5 +1,452 @@
require "rails_helper"
RSpec.describe WebhooksController, type: :controller do
- pending
+ let(:valid_session) { {} }
+ let!(:registry) { create(:registry) }
+ let(:user) { create(:user) }
+ let(:viewer) { create(:user) }
+ let(:contributor) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:team) do
+ create(:team,
+ owners: [owner],
+ viewers: [user, viewer],
+ contributors: [contributor])
+ end
+ let(:namespace) do
+ create(
+ :namespace,
+ team: team,
+ description: "short test description",
+ registry: registry)
+ end
+ let!(:webhook) do
+ create(
+ :webhook,
+ namespace: namespace
+ )
+ end
+ let!(:webhook_header) { create(:webhook_header, webhook: webhook) }
+ let!(:webhook_delivery) { create(:webhook_delivery, webhook: webhook) }
+
+ before :each do
+ sign_in user
+ end
+
+ describe "GET #index" do
+ it "assigns all webhooks as @webhooks" do
+ get :index, { namespace_id: namespace }, valid_session
+ expect(assigns(:webhooks)).to match_array(
+ [Webhook.find_by(namespace: namespace)])
+ end
+
+ it "paginates webhooks" do
+ get :index, { namespace_id: namespace }, valid_session
+ expect(assigns(:webhooks)).to respond_to(:total_pages)
+ end
+ end
+
+ describe "GET #show" do
+ it "should paginate webhook deliveries" do
+ sign_in owner
+ get :show, namespace_id: namespace.id, id: webhook.id
+
+ expect(assigns(:deliveries)).to respond_to(:total_pages)
+ end
+
+ it "allows team members to view the page" do
+ sign_in owner
+ get :show, namespace_id: namespace.id, id: webhook.id
+
+ expect(assigns(:webhook)).to eq(webhook)
+ expect(response.status).to eq 200
+ end
+
+ it "blocks users that are not part of the team" do
+ sign_in create(:user)
+ get :show, namespace_id: namespace.id, id: webhook.id
+
+ expect(response.status).to eq 401
+ end
+ end
+
+ describe "DELETE #destroy" do
+ render_views
+
+ context "as a contributor of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in contributor
+
+ expect do
+ delete :destroy, namespace_id: namespace.id, id: webhook.id
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "as a viewer of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in viewer
+
+ expect do
+ delete :destroy, namespace_id: namespace.id, id: webhook.id
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+
+ it "shows an error message" do
+ sign_in viewer
+ xhr :delete, :destroy, namespace_id: namespace.id, id: webhook.id
+ expect(response.body).to include("You are not authorized to access this page")
+ end
+ end
+
+ context "as a generic user not part of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in create(:user)
+
+ expect do
+ delete :destroy, namespace_id: namespace.id, id: webhook.id
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+ end
+
+ it "should delete a webhook" do
+ sign_in owner
+ delete :destroy, namespace_id: namespace.id, id: webhook.id
+ expect(response.status).to eq 302
+ end
+ end
+
+ describe "PUT #toggle_enabled" do
+ it "allows the owner of the team to change the enabled attribute" do
+ sign_in owner
+ put :toggle_enabled, namespace_id: namespace.id, id: webhook.id, format: :js
+
+ webhook.reload
+ expect(webhook).to_not be_enabled
+ expect(response.status).to eq 200
+ end
+
+ it "blocks users that are not part of the team" do
+ sign_in create(:user)
+ put :toggle_enabled, namespace_id: namespace.id, id: webhook.id, format: :js
+
+ expect(response.status).to eq 401
+ end
+ end
+
+ describe "POST #create" do
+ render_views
+ let(:valid_attributes) do
+ {
+ namespace: namespace.id,
+ url: "example.org"
+ }
+ end
+
+ let(:invalid_attributes) do
+ {
+ namespace: namespace.id
+ }
+ end
+
+ context "as a contributor of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in contributor
+ post_params = {
+ webhook: valid_attributes,
+ namespace_id: namespace,
+ format: :js
+ }
+
+ expect do
+ post :create, post_params
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "as a viewer of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in viewer
+ post_params = {
+ webhook: valid_attributes,
+ namespace_id: namespace,
+ format: :js
+ }
+
+ expect do
+ post :create, post_params
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+
+ it "shows an error message" do
+ sign_in viewer
+ post_params = { webhook: valid_attributes, namespace_id: namespace }
+ xhr :post, :create, post_params
+ expect(response.body).to include("You are not authorized to access this page")
+ end
+ end
+
+ context "as a generic user not part of the team that is going to control webhooks" do
+ it "blocks access" do
+ sign_in create(:user)
+ post_params = {
+ webhook: valid_attributes,
+ namespace_id: namespace,
+ format: :js
+ }
+
+ expect do
+ post :create, post_params
+ end.not_to change(Webhook, :count)
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "with valid params" do
+ before :each do
+ sign_in owner
+ @post_params = {
+ webhook: valid_attributes,
+ namespace_id: namespace,
+ format: :js
+ }
+ end
+
+ it "creates a new webhook" do
+ expect do
+ post :create, @post_params
+ end.to change(Webhook, :count).by(1)
+ expect(assigns(:webhook).namespace).to eq(namespace)
+ expect(assigns(:webhook).url).to eq("http://#{valid_attributes[:url]}")
+ expect(assigns(:webhook).enabled).to be_falsy
+ end
+
+ it "assigns a newly created webhook as @webhook" do
+ post :create, @post_params
+ expect(assigns(:webhook)).to be_a(Webhook)
+ expect(assigns(:webhook)).to be_persisted
+ end
+
+ it "creates a new webhook with the given username" do
+ @post_params[:webhook]["username"] = "user"
+ expect do
+ post :create, @post_params
+ end.to change(Webhook, :count).by(1)
+ expect(assigns(:webhook).namespace).to eq(namespace)
+ expect(assigns(:webhook).url).to eq("http://#{valid_attributes[:url]}")
+ expect(assigns(:webhook).username).to eq("user")
+ end
+
+ it "creates a new webhook with the given password" do
+ @post_params[:webhook]["password"] = "password"
+ expect do
+ post :create, @post_params
+ end.to change(Webhook, :count).by(1)
+ expect(assigns(:webhook).namespace).to eq(namespace)
+ expect(assigns(:webhook).url).to eq("http://#{valid_attributes[:url]}")
+ expect(assigns(:webhook).password).to eq("password")
+ end
+
+ it "creates a new webhook with the POST method" do
+ @post_params[:webhook]["request_method"] = "POST"
+ expect do
+ post :create, @post_params
+ end.to change(Webhook, :count).by(1)
+ expect(assigns(:webhook).namespace).to eq(namespace)
+ expect(assigns(:webhook).url).to eq("http://#{valid_attributes[:url]}")
+ expect(assigns(:webhook).request_method).to eq("POST")
+ end
+
+ it "creates a new webhook with the JSON content type" do
+ @post_params[:webhook]["content_type"] = "application/json"
+ expect do
+ post :create, @post_params
+ end.to change(Webhook, :count).by(1)
+ expect(assigns(:webhook).namespace).to eq(namespace)
+ expect(assigns(:webhook).url).to eq("http://#{valid_attributes[:url]}")
+ expect(assigns(:webhook).content_type).to eq("application/json")
+ end
+ end
+
+ context "with invalid params" do
+ before :each do
+ sign_in owner
+ end
+
+ it "assigns a newly created but unsaved webhook as @webhook" do
+ post_params = {
+ webhook: invalid_attributes,
+ namespace_id: namespace,
+ format: :js
+ }
+ post :create, post_params
+ expect(assigns(:webhook)).to be_a_new(Webhook)
+ expect(response.status).to eq(422)
+ end
+
+ it "fails to create a webhook with an invalid request method" do
+ post_params = {
+ webhook: invalid_attributes,
+ namespace_id: namespace,
+ request_method: "PUT",
+ format: :js
+ }
+ post :create, post_params
+ expect(assigns(:webhook)).to be_a_new(Webhook)
+ expect(response.status).to eq(422)
+ end
+
+ it "fails to create a webhook with an invalid content type" do
+ post_params = {
+ webhook: invalid_attributes,
+ namespace_id: namespace,
+ content_type: "text/plain",
+ format: :js
+ }
+ post :create, post_params
+ expect(assigns(:webhook)).to be_a_new(Webhook)
+ expect(response.status).to eq(422)
+ end
+ end
+ end
+
+ describe "PATCH #update" do
+ it "does not allow to change the url by viewers" do
+ team = create(:team)
+ user = create(:user)
+ TeamUser.create(team: team, user: user, role: TeamUser.roles["viewers"])
+ sign_in user
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ url: "port.us" }, format: "js"
+ expect(response.status).to eq(401)
+ end
+
+ it "does not allow to change the request method by viewers" do
+ team = create(:team)
+ user = create(:user)
+ TeamUser.create(team: team, user: user, role: TeamUser.roles["viewers"])
+ sign_in user
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ request_method: "POST" }, format: "js"
+ expect(response.status).to eq(401)
+ end
+
+ it "does not allow to change the content type by viewers" do
+ team = create(:team)
+ user = create(:user)
+ TeamUser.create(team: team, user: user, role: TeamUser.roles["viewers"])
+ sign_in user
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ content_type: "application/json" }, format: "js"
+ expect(response.status).to eq(401)
+ end
+
+ it "does not allow to change the username by viewers" do
+ team = create(:team)
+ user = create(:user)
+ TeamUser.create(team: team, user: user, role: TeamUser.roles["viewers"])
+ sign_in user
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ username: "alice" }, format: "js"
+ expect(response.status).to eq(401)
+ end
+
+ it "does not allow to change the password by viewers" do
+ team = create(:team)
+ user = create(:user)
+ TeamUser.create(team: team, user: user, role: TeamUser.roles["viewers"])
+ sign_in user
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ password: "supersecure" }, format: "js"
+ expect(response.status).to eq(401)
+ end
+
+ it "does allow to change the url by owners" do
+ sign_in owner
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ url: "port.us" }, format: "js"
+ expect(response.status).to eq(200)
+ end
+
+ it "fails when providing invalid parameters" do
+ sign_in owner
+ patch :update, id: webhook.id, namespace_id: namespace.id, webhook: {
+ url: "" }, format: "js"
+ expect(response.status).to eq(422)
+ end
+ end
+
+ describe "activity tracking" do
+ before :each do
+ sign_in owner
+ end
+
+ it "tracks webhook creation" do
+ post_params = {
+ webhook: { url: "example.org" },
+ namespace_id: namespace,
+ format: :js
+ }
+
+ expect do
+ post :create, post_params
+ end.to change(PublicActivity::Activity, :count).by(1)
+
+ activity = PublicActivity::Activity.last
+ expect(activity.key).to eq("webhook.create")
+ expect(activity.owner).to eq(owner)
+ expect(activity.trackable).to eq(Webhook.last)
+ end
+
+ it "tracks set webhook enabled" do
+ webhook.update_attributes(enabled: false)
+
+ expect do
+ put :toggle_enabled, namespace_id: namespace.id, id: webhook.id, format: :js
+ end.to change(PublicActivity::Activity, :count).by(1)
+
+ activity = PublicActivity::Activity.last
+ expect(activity.key).to eq("webhook.enabled")
+ expect(activity.owner).to eq(owner)
+ expect(activity.trackable).to eq(webhook)
+ end
+
+ it "tracks set webhook disabled" do
+ webhook.update_attributes(enabled: true)
+
+ expect do
+ put :toggle_enabled, namespace_id: namespace.id, id: webhook.id, format: :js
+ end.to change(PublicActivity::Activity, :count).by(1)
+
+ activity = PublicActivity::Activity.last
+ expect(activity.key).to eq("webhook.disabled")
+ expect(activity.owner).to eq(owner)
+ expect(activity.trackable).to eq(webhook)
+ end
+
+ it "tracks updates to the webhook" do
+ expect do
+ patch :update,
+ id: webhook.id,
+ namespace_id: namespace.id,
+ webhook: { url: "port.us" },
+ format: "js"
+ end.to change(PublicActivity::Activity, :count).by(1)
+ end
+
+ it "tracks removal of the webhook" do
+ expect do
+ delete :destroy,
+ id: webhook.id,
+ namespace_id: namespace.id,
+ webhook: { url: "port.us" },
+ format: "js"
+ end.to change(PublicActivity::Activity, :count).by(1)
+ end
+ end
end
diff --git a/spec/factories/activities.rb b/spec/factories/activities.rb
index cc43d7358..866ef36a1 100644
--- a/spec/factories/activities.rb
+++ b/spec/factories/activities.rb
@@ -66,4 +66,15 @@
parameters Hash.new(application: "test application")
end
+ factory :activity_webhook_create, class: PublicActivity::Activity do
+ trackable_type "Webhook"
+ owner_type "User"
+ key "webhook.create"
+ end
+
+ factory :activity_webhook_destroy, class: PublicActivity::Activity do
+ trackable_type "Webhook"
+ owner_type "User"
+ key "webhook.destroy"
+ end
end
diff --git a/spec/factories/webhook_deliveries.rb b/spec/factories/webhook_deliveries.rb
index 4e7aa74f6..2b5a74ff9 100644
--- a/spec/factories/webhook_deliveries.rb
+++ b/spec/factories/webhook_deliveries.rb
@@ -5,7 +5,7 @@
# id :integer not null, primary key
# webhook_id :integer
# uuid :string(255)
-# status :string(255)
+# status :integer
# request_header :text(65535)
# request_body :text(65535)
# response_header :text(65535)
@@ -21,5 +21,8 @@
FactoryGirl.define do
factory :webhook_delivery do
+ sequence(:uuid) { |n| "uuid_#{n}" }
+ request_body "{}"
+ status 404
end
end
diff --git a/spec/factories/webhooks.rb b/spec/factories/webhooks.rb
index 65e270cec..9a5405a08 100644
--- a/spec/factories/webhooks.rb
+++ b/spec/factories/webhooks.rb
@@ -9,7 +9,7 @@
# password :string(255)
# request_method :integer
# content_type :integer
-# enabled :boolean
+# enabled :boolean default("0")
# created_at :datetime not null
# updated_at :datetime not null
#
@@ -20,9 +20,11 @@
FactoryGirl.define do
factory :webhook do
- url "http://localhost/webhook"
+ url "http://www.example.com"
request_method "POST"
content_type "application/json"
enabled true
+ username ""
+ password ""
end
end
diff --git a/spec/jobs/catalog_job_spec.rb b/spec/jobs/catalog_job_spec.rb
index 8dfc0ce07..5535dc9f0 100644
--- a/spec/jobs/catalog_job_spec.rb
+++ b/spec/jobs/catalog_job_spec.rb
@@ -42,6 +42,8 @@ def update_registry!(catalog)
end
it "raises an exception when there has been a problem in /v2/_catalog" do
+ VCR.turn_on!
+
create(:registry, "hostname" => "registry.test.lan")
VCR.use_cassette("registry/get_missing_catalog_endpoint", record: :none) do
@@ -52,6 +54,8 @@ def update_registry!(catalog)
end
it "performs the job as expected" do
+ VCR.turn_on!
+
registry = create(:registry, "hostname" => "registry.test.lan")
VCR.use_cassette("registry/get_registry_catalog", record: :none) do
@@ -69,6 +73,8 @@ def update_registry!(catalog)
end
it "handles registries even if there some namespaces missing" do
+ VCR.turn_on!
+
registry = create(:registry, "hostname" => "registry.test.lan")
VCR.use_cassette("registry/get_registry_catalog_namespace_missing", record: :none) do
diff --git a/spec/lib/portus/registry_client_spec.rb b/spec/lib/portus/registry_client_spec.rb
index 596a955c4..eca879042 100644
--- a/spec/lib/portus/registry_client_spec.rb
+++ b/spec/lib/portus/registry_client_spec.rb
@@ -67,6 +67,7 @@ def fetch_link_test(header)
end
it "fails if the registry has authentication enabled and no credentials are set" do
+ VCR.turn_on!
path = ""
registry = RegistryClientMissingCredentials.new(registry_server)
VCR.use_cassette("registry/missing_credentials", record: :none) do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 5a030a443..5e1d4e5d2 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -97,7 +97,7 @@
let!(:registry) { create(:registry) }
let!(:owner) { create(:user) }
let!(:team) { create(:team, owners: [owner]) }
- let!(:namespace) { create(:namespace, team: team) }
+ let!(:namespace) { create(:namespace, team: team, registry: registry) }
let!(:repo) { create(:repository, namespace: namespace) }
it "works for global namespaces" do
@@ -112,5 +112,20 @@
expect(ns.id).to eq namespace.id
expect(name).to eq repo.name
end
+
+ context "when providing a registry" do
+ it "works for global namespaces" do
+ ns = Namespace.find_by(global: true)
+ namespace, name = Namespace.get_from_name(repo.name, registry)
+ expect(namespace.id).to eq ns.id
+ expect(name).to eq repo.name
+ end
+
+ it "works for user namespaces" do
+ ns, name = Namespace.get_from_name("#{namespace.name}/#{repo.name}", registry)
+ expect(ns.id).to eq namespace.id
+ expect(name).to eq repo.name
+ end
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index d1398f37c..368db34f1 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -75,6 +75,10 @@ def get_url(repo, tag)
let(:registry) { create(:registry, hostname: "registry.test.lan") }
let(:user) { create(:user) }
+ before :each do
+ VCR.turn_on!
+ end
+
context "adding an existing repo/tag" do
it "does not add a new activity when an already existing repo/tag already existed" do
event = { "actor" => { "name" => user.username } }
diff --git a/spec/models/webhook_delivery_spec.rb b/spec/models/webhook_delivery_spec.rb
index 8fc77dfcf..26392e4c9 100644
--- a/spec/models/webhook_delivery_spec.rb
+++ b/spec/models/webhook_delivery_spec.rb
@@ -5,7 +5,7 @@
# id :integer not null, primary key
# webhook_id :integer
# uuid :string(255)
-# status :string(255)
+# status :integer
# request_header :text(65535)
# request_body :text(65535)
# response_header :text(65535)
@@ -26,4 +26,62 @@
it { should belong_to(:webhook) }
it { should validate_uniqueness_of(:uuid).scoped_to(:webhook_id) }
+
+ describe "success?" do
+ let!(:registry) { create(:registry) }
+ let!(:owner) { create(:user) }
+ let!(:team) { create(:team, owners: [owner]) }
+ let!(:namespace) { create(:namespace, team: team) }
+ let!(:webhook) { create(:webhook, namespace: namespace) }
+ let!(:webhook_delivery) { create(:webhook_delivery, webhook: webhook) }
+
+ it "returns true for HTTP code 200" do
+ webhook_delivery.status = 200
+ expect(webhook_delivery.success?).to be true
+ end
+
+ it "returns false for HTTP codes other than 200" do
+ webhook_delivery.status = 418
+ expect(webhook_delivery.success?).to be false
+ end
+ end
+
+ describe "retrigger" do
+ let!(:registry) { create(:registry) }
+ let!(:owner) { create(:user) }
+ let!(:team) { create(:team, owners: [owner]) }
+ let!(:namespace) { create(:namespace, team: team) }
+ let!(:webhook_noauth) { create(:webhook, namespace: namespace) }
+ let!(:webhook_auth) do
+ create(:webhook, namespace: namespace, username: "username", password: "password")
+ end
+ let!(:webhook_header) do
+ create(:webhook_header, webhook: webhook_noauth, name: "foo", value: "bar")
+ end
+
+ before :each do
+ stub_request(:POST, "username:password@www.example.com")
+ .to_return(
+ status: 200,
+ body: %({"hello": "world"}),
+ headers: { "lorem" => "ipsum" }
+ )
+ stub_request(:POST, "www.example.com").to_return(
+ status: 200,
+ body: %({"hello": "world"}),
+ headers: { "Lorem" => "ipsum" }
+ )
+ end
+
+ it "should resend HTTP requests" do
+ create :webhook_delivery, webhook: webhook_auth
+ create :webhook_delivery, webhook: webhook_noauth
+ WebhookDelivery.find_each do |delivery|
+ delivery.retrigger
+ expect(delivery.status).to eq 200
+ expect(delivery.response_body).to eq %({"hello": "world"})
+ expect(delivery.response_header).to eq "Lorem: ipsum"
+ end
+ end
+ end
end
diff --git a/spec/models/webhook_spec.rb b/spec/models/webhook_spec.rb
index a6443b674..2d0711008 100644
--- a/spec/models/webhook_spec.rb
+++ b/spec/models/webhook_spec.rb
@@ -9,7 +9,7 @@
# password :string(255)
# request_method :integer
# content_type :integer
-# enabled :boolean
+# enabled :boolean default("0")
# created_at :datetime not null
# updated_at :datetime not null
#
@@ -30,8 +30,63 @@
it { should belong_to(:namespace) }
it { should validate_presence_of(:url) }
+ it { should_not allow_value("won't work").for(:url) }
it { should define_enum_for(:request_method) }
it { should allow_value(*request_methods).for(:request_method) }
it { should define_enum_for(:content_type) }
it { should allow_value(*content_types).for(:content_type) }
+
+ describe "push event" do
+ let!(:registry) { create(:registry) }
+ let!(:owner) { create(:user) }
+ let!(:team) { create(:team, owners: [owner]) }
+ let!(:namespace) { create(:namespace, team: team, registry: registry) }
+ let!(:repo) { create(:repository, namespace: namespace) }
+ let!(:event) do
+ {
+ "request" => { "host" => "#{registry.hostname}" },
+ "target" => { "repository" => "#{namespace.name}/#{repo.name}" }
+ }
+ end
+
+ before :each do
+ stub_request(:POST, "username:password@www.example.com")
+ .to_return(status: 200)
+ stub_request(:POST, "www.example.com").to_return(status: 200)
+ end
+
+ context "triggering a webhook" do
+ let!(:webhook_noauth) { create(:webhook, namespace: namespace) }
+ let!(:webhook_auth) do
+ create(:webhook, namespace: namespace, username: "username", password: "password")
+ end
+ let!(:webhook_header) do
+ create(:webhook_header, webhook: webhook_auth, name: "foo", value: "bar")
+ end
+
+ it "should work when given user credentials" do
+ Webhook.handle_push_event(event)
+ delivery = WebhookDelivery.find_by(webhook: webhook_auth)
+ expect(delivery.status).to eq 200
+ expect(JSON.parse(delivery.request_body)).to eq event
+ end
+
+ it "should work when providing no user credentials" do
+ Webhook.handle_push_event(event)
+ delivery = WebhookDelivery.find_by(webhook: webhook_noauth)
+ expect(delivery.status).to eq 200
+ expect(JSON.parse(delivery.request_body)).to eq event
+ end
+
+ it "should fail in the given namespace cannot be found" do
+ event["target"]["repository"] = "unknown_namespace/unknown_repo"
+ expect(Webhook.handle_push_event(event)).to be nil
+ end
+ end
+
+ it "should skip disabled webhooks" do
+ Webhook.handle_push_event(event)
+ expect(WebhookDelivery.all).to be_empty
+ end
+ end
end
diff --git a/spec/policies/public_activity/activity_policy_spec.rb b/spec/policies/public_activity/activity_policy_spec.rb
index b86da7839..cc329b5af 100644
--- a/spec/policies/public_activity/activity_policy_spec.rb
+++ b/spec/policies/public_activity/activity_policy_spec.rb
@@ -4,12 +4,15 @@
let(:user) { create(:user) }
let(:another_user) { create(:user) }
+ let(:viewer) { create(:user) }
+ let(:contributor) { create(:user) }
let(:activity_owner) { create(:user) }
let(:registry) { create(:registry) }
let(:namespace) { create(:namespace, registry: registry, team: team) }
- let(:team) { create(:team, owners: [user]) }
+ let(:team) { create(:team, owners: [user], contributors: [contributor], viewers: [viewer]) }
let(:repository) { create(:repository, namespace: namespace) }
let(:tag) { create(:tag, repository: repository) }
+ let(:webhook) { create(:webhook, namespace: namespace, url: "http://example.com") }
subject { described_class }
@@ -124,6 +127,19 @@
expect(Pundit.policy_scope(user, PublicActivity::Activity).to_a).to match_array(activities)
end
+
+ it "returns pertinent webhook activities" do
+ activities = [
+ create_activity_webhook_create(webhook, activity_owner),
+ create_activity_webhook_destroy(webhook, activity_owner, namespace)
+ ]
+
+ expect(Pundit.policy_scope(user, PublicActivity::Activity).to_a).to match_array(activities)
+ expect(Pundit.policy_scope(contributor, PublicActivity::Activity).to_a)
+ .to match_array(activities)
+ expect(Pundit.policy_scope(viewer, PublicActivity::Activity).to_a).to match_array(activities)
+ expect(Pundit.policy_scope(another_user, PublicActivity::Activity).to_a).to be_empty
+ end
end
private
@@ -165,4 +181,20 @@ def create_activity_application_token_created(owner)
owner_id: owner.id)
end
+ def create_activity_webhook_create(webhook, activity_owner)
+ create(:activity_webhook_create,
+ trackable_id: webhook.id,
+ owner_id: activity_owner.id)
+ end
+
+ def create_activity_webhook_destroy(webhook, activity_owner, namespace)
+ create(:activity_webhook_destroy,
+ trackable_id: webhook.id,
+ owner_id: activity_owner.id,
+ parameters: { namespace_id: namespace.id,
+ namespace_name: namespace.name,
+ webhook_url: webhook.url,
+ webhook_host: webhook.host }
+ )
+ end
end
diff --git a/spec/policies/webhook_header_policy_spec.rb b/spec/policies/webhook_header_policy_spec.rb
deleted file mode 100644
index e257d114a..000000000
--- a/spec/policies/webhook_header_policy_spec.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require "rails_helper"
-
-describe WebhookHeaderPolicy do
- pending
-end
diff --git a/spec/policies/webhook_policy_spec.rb b/spec/policies/webhook_policy_spec.rb
index e5a191e75..153bd5e57 100644
--- a/spec/policies/webhook_policy_spec.rb
+++ b/spec/policies/webhook_policy_spec.rb
@@ -1,5 +1,81 @@
require "rails_helper"
describe WebhookPolicy do
- pending
+ subject { described_class }
+
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:viewer) { create(:user) }
+ let(:contributor) { create(:user) }
+ let(:team) do
+ create(:team,
+ owners: [owner],
+ contributors: [contributor],
+ viewers: [viewer])
+ end
+ let(:namespace) do
+ create(
+ :namespace,
+ description: "short test description.",
+ registry: @registry,
+ team: team)
+ end
+ let(:webhook) { create(:webhook, namespace: namespace) }
+
+ before :each do
+ @admin = create(:admin)
+ @registry = create(:registry)
+ end
+
+ permissions :toggle_enabled? do
+ it "allows admin to change it" do
+ expect(subject).to permit(@admin, webhook)
+ end
+
+ it "allows owner to change it" do
+ expect(subject).to permit(owner, webhook)
+ end
+
+ it "disallows contributor to change it" do
+ expect(subject).to_not permit(contributor, webhook)
+ end
+
+ it "disallows user to change it" do
+ expect(subject).to_not permit(user, webhook)
+ end
+
+ it "disallows viewer to change it" do
+ expect(subject).to_not permit(viewer, webhook)
+ end
+ end
+
+ describe "scope" do
+ before :each do
+ webhook
+ end
+
+ it "shows all webhooks" do
+ expected = Webhook.all
+ expect(Pundit.policy_scope(@admin, Webhook).to_a).to match_array(expected)
+ end
+
+ it "shows webhooks to owner" do
+ expected = webhook
+ expect(Pundit.policy_scope(owner, Webhook).to_a).to match_array(expected)
+ end
+
+ it "shows webhooks to contributor" do
+ expected = webhook
+ expect(Pundit.policy_scope(contributor, Webhook).to_a).to match_array(expected)
+ end
+
+ it "shows webhooks to viewer" do
+ expected = webhook
+ expect(Pundit.policy_scope(viewer, Webhook).to_a).to match_array(expected)
+ end
+
+ it "does not show webhooks to user" do
+ expect(Pundit.policy_scope(user, Webhook).to_a).to be_empty
+ end
+ end
end