pushcart 0.0.1.proto1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +7 -0
- data/Rakefile +40 -0
- data/app/assets/javascripts/pushcart/application.js +13 -0
- data/app/assets/stylesheets/pushcart/application.css +15 -0
- data/app/controllers/concerns/pushcart/getable.rb +127 -0
- data/app/controllers/concerns/pushcart/postable.rb +113 -0
- data/app/controllers/pushcart/application_controller.rb +4 -0
- data/app/controllers/pushcart/pub_sub/base_controller.rb +4 -0
- data/app/controllers/pushcart/pub_sub/subscriptions_controller.rb +8 -0
- data/app/helpers/pushcart/application_helper.rb +4 -0
- data/app/jobs/pushcart/subscription_renewal_job.rb +9 -0
- data/app/views/layouts/pushcart/application.html.erb +14 -0
- data/config/routes.rb +6 -0
- data/lib/pushcart.rb +16 -0
- data/lib/pushcart/composable.rb +10 -0
- data/lib/pushcart/connectable.rb +34 -0
- data/lib/pushcart/deliverable.rb +5 -0
- data/lib/pushcart/engine.rb +5 -0
- data/lib/pushcart/errors.rb +4 -0
- data/lib/pushcart/requestable.rb +68 -0
- data/lib/pushcart/stateable.rb +166 -0
- data/lib/pushcart/subscribable.rb +25 -0
- data/lib/pushcart/version.rb +3 -0
- data/lib/tasks/pushcart_tasks.rake +4 -0
- data/test/connectable_test.rb +24 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +15 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/models/subscription.rb +9 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +26 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +41 -0
- data/test/dummy/config/environments/production.rb +79 -0
- data/test/dummy/config/environments/test.rb +42 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config/secrets.yml +22 -0
- data/test/dummy/db/migrate/20151016222203_create_subscriptions.rb +59 -0
- data/test/dummy/db/schema.rb +51 -0
- data/test/dummy/db/test.sqlite3 +0 -0
- data/test/dummy/log/development.log +0 -0
- data/test/dummy/log/test.log +5270 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/fixtures/subscriptions.yml +6 -0
- data/test/integration/content_delivery_test.rb +58 -0
- data/test/integration/hub_request_test.rb +127 -0
- data/test/pushcart_test.rb +7 -0
- data/test/requestable_test.rb +7 -0
- data/test/routes_test.rb +9 -0
- data/test/state_test.rb +13 -0
- data/test/test_helper.rb +27 -0
- metadata +349 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
module Pushcart
|
2
|
+
module Requestable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def request_subscription
|
6
|
+
request(:subscription)
|
7
|
+
end
|
8
|
+
|
9
|
+
def request_renewal
|
10
|
+
update_columns(
|
11
|
+
renewal_requested_at: nil,
|
12
|
+
renewal_request_responded_at: nil,
|
13
|
+
renewal_request_response_code: nil,
|
14
|
+
renewal_request_response_error: nil,
|
15
|
+
renewal_denied_at: nil,
|
16
|
+
renewal_denial_reason: nil,
|
17
|
+
renewal_challenged_at: nil,
|
18
|
+
renewal_verified_at: nil,
|
19
|
+
)
|
20
|
+
|
21
|
+
request(:renewal)
|
22
|
+
end
|
23
|
+
|
24
|
+
def request_unsubscription
|
25
|
+
request(:unsubscription)
|
26
|
+
end
|
27
|
+
|
28
|
+
def request(type)
|
29
|
+
modes = {
|
30
|
+
subscription: "subscribe",
|
31
|
+
renewal: "subscribe",
|
32
|
+
unsubscription: "unsubscribe"
|
33
|
+
}
|
34
|
+
|
35
|
+
mode = mode[type]
|
36
|
+
prefix = types.to_s
|
37
|
+
|
38
|
+
# Subscriber Sends Subscription Request
|
39
|
+
|
40
|
+
update_attribute(:"#{prefix}_requested_at", Time.now)
|
41
|
+
response = connection(mode: mode).post(hub)
|
42
|
+
|
43
|
+
# Subscription Response Details
|
44
|
+
|
45
|
+
update_attribute(:"#{prefix}_request_responded_at", Time.now)
|
46
|
+
update_attribute(:"#{prefix}_request_response_code", response.status)
|
47
|
+
|
48
|
+
(response.status == 202) ? _accepted(response) : _rejected(response)
|
49
|
+
end
|
50
|
+
|
51
|
+
# If the request was accepted, trigger the +receive+ event on the state
|
52
|
+
# machine (which will transition to different states depending on whether
|
53
|
+
# this is a subscribe, renew, or unsubscribe request).
|
54
|
+
def _accepted(response)
|
55
|
+
receive!
|
56
|
+
end
|
57
|
+
|
58
|
+
# If the request was not accepted for any reason (rejected by the hub, or
|
59
|
+
# the response returned any status other than 202), trigger the +reject+
|
60
|
+
# event. This will move the subscription to a `rejected` state regardless
|
61
|
+
# of what kind of request was made.
|
62
|
+
# TODO Make sure unsub. rejections are being handled correctly.
|
63
|
+
def _rejected(response)
|
64
|
+
update_attribute(:"#{prefix}_request_response_error", request_response.body)
|
65
|
+
reject!
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Pushcart
|
2
|
+
module Stateable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include AASM
|
5
|
+
|
6
|
+
included do
|
7
|
+
aasm do
|
8
|
+
state :undiscovered, initial: true
|
9
|
+
# state :discovered
|
10
|
+
#
|
11
|
+
# state :requesting
|
12
|
+
# state :received
|
13
|
+
# state :verified
|
14
|
+
#
|
15
|
+
# state :rerequesting
|
16
|
+
# state :rereceived
|
17
|
+
#
|
18
|
+
# state :unrequesting
|
19
|
+
# state :unreceived
|
20
|
+
#
|
21
|
+
# state :canceled
|
22
|
+
# state :rejected
|
23
|
+
# state :denied
|
24
|
+
# state :unsubscribed
|
25
|
+
# state :lapsed
|
26
|
+
#
|
27
|
+
# event :cancel do
|
28
|
+
# transitions from: [:undiscovered, :discovered, :requesting, :received], to: :canceled
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# event :discover, if: :discoverable? do
|
32
|
+
# transitions from: :undiscovered, to: :discovered
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# event :request do
|
36
|
+
# transitions from: :discovered, to: :requesting
|
37
|
+
# transitions from: :verified, to: :rerequesting
|
38
|
+
#
|
39
|
+
# # If the request state change is successful, actually trigger the
|
40
|
+
# # request based on whether it's a renewal or not.
|
41
|
+
# success do
|
42
|
+
# rerequesting? ? request_renewal : request_subscription
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# event :receive do
|
47
|
+
# transitions from: :requesting, to: :received
|
48
|
+
# transitions from: :rerequesting, to: :rereceived
|
49
|
+
# transitions from: :unrequesting, to: :unreceived
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# event :verify, unless: :terminated?, success: Proc.new {|*args| _verify_success(*args) } do
|
53
|
+
# transitions from: [:requesting, :received, :rerequesting, :rereceived], to: :verified
|
54
|
+
# transitions from: [:unrequesting, :unreceived], to: :unsubscribed
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# event :reject do
|
58
|
+
# transitions from: [:requesting, :rerequesting], to: :rejected
|
59
|
+
# # transitions from: :unrequesting, to: :?? TODO
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# event :unsubscribe, unless: :terminated? do
|
63
|
+
# transitions from: [:verified, :rerequesting, :rereceived], to: :unrequesting
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# event :deny do
|
67
|
+
# # TODO `after` seems like the wrong callback here, but the docs say it
|
68
|
+
# # will happen before the transition...
|
69
|
+
# transitions from: [:requesting, :received, :verified, :rerequesting, :rereceived, :unrequesting, :unreceived], to: :denied, after: Proc.new {|*args| _deny_after(*args) }
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# event :lapse do
|
73
|
+
# transitions from: [:verified, :rerequesting, :rereceived, :unrequesting, :unreceived], to: :lapsed
|
74
|
+
# end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# TODO Make sure this callback is happening at the right point in the chain
|
79
|
+
def _verify_success(lease)
|
80
|
+
if unsubscribed?
|
81
|
+
update_attribute(:unsubscription_verified_at, Time.now)
|
82
|
+
elsif renewal_requested_at
|
83
|
+
# TODO Find a better way to know if this is a renewal
|
84
|
+
increment!(:renewals)
|
85
|
+
update_attribute(:renewal_verified_at, Time.now)
|
86
|
+
else
|
87
|
+
update_attribute(:subscription_verified_at, Time.now)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# TODO Make sure this callback is happening at the right point in the chain
|
92
|
+
def _deny_after(reason)
|
93
|
+
# NOTE: Unsubscribe requests should never be denied during validation, so
|
94
|
+
# even if a denial request comes in while the subscription is in an
|
95
|
+
# `unsubscribing?` state, it is assumed that the denial is unrelated to
|
96
|
+
# that request, and the denial will be attached to either the originally
|
97
|
+
# subscription, or the most recent renewal.
|
98
|
+
if renewing? || renewed?
|
99
|
+
update_attribute(:renewal_denied_at, Time.now)
|
100
|
+
update_attribute(:renewal_denial_reason, reason)
|
101
|
+
else
|
102
|
+
update_attribute(:subscription_denied_at, Time.now)
|
103
|
+
update_attribute(:subscription_denial_reason, reason)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def discoverable?
|
108
|
+
# TODO has all the info it needs to make a request
|
109
|
+
end
|
110
|
+
|
111
|
+
# Returns `true` if the subscription has a pending initial subscription
|
112
|
+
# request or renewal request open with the hub.
|
113
|
+
def subscribing?
|
114
|
+
!expired && (renewing? || requesting? || received?)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns `true` if the subscription has a pending subscription renewal
|
118
|
+
# request open with the hub.
|
119
|
+
def renewing?
|
120
|
+
!expired? && (rerequesting? || rereceived?)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return `true` if this subscription has ever been successfully renewed,
|
124
|
+
# even if it has since been terminated (by expiring, unsubscribing, etc).
|
125
|
+
def renewed?
|
126
|
+
renewals > 0
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns `true` if the subscription has a pending unsubscription
|
130
|
+
# request open with the hub.
|
131
|
+
def unsubscribing?
|
132
|
+
!expired? && (unrequesting? || unreceived?)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Returns `true` if the subscription has been verified, and has not expired
|
136
|
+
# or been otherwise invalidated (denied, unsubscribed, etc). When a
|
137
|
+
# subscription is `active` it is expecting to get updates from the hub.
|
138
|
+
# Once a subscription has been verified, it remains active throughout
|
139
|
+
# the renewal and unsubscribe processes unless something invalidates it.
|
140
|
+
def active?
|
141
|
+
!expired? && (verified? || renewing? || unsubscribing?)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Returns `true` if the subscription (and any subsequent renewals) have
|
145
|
+
# exceeded the lease provided by the hub. Because expiration is passive and
|
146
|
+
# has results in no callbacks, it is possible that a subscription expired
|
147
|
+
# without its state being updated to reflect the change. Any time +expired?+
|
148
|
+
# is called, it will ensure the state is in sync with reality.
|
149
|
+
# @return [true, false]
|
150
|
+
def expired?
|
151
|
+
return true if lapsed?
|
152
|
+
|
153
|
+
if expired_at <= Time.now
|
154
|
+
lapse if may_lapse?
|
155
|
+
|
156
|
+
return true
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Returns `true` if the subscription has been canceled for any reason, such
|
161
|
+
# as expiring or being ended by the hub.
|
162
|
+
def terminated?
|
163
|
+
canceled? || rejected? || denied? || unsubscribed? || lapsed? || expired?
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Pushcart
|
2
|
+
module Subscribable
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
include Composable
|
5
|
+
|
6
|
+
included do
|
7
|
+
# Callbacks
|
8
|
+
# before_validation(on: :create) do
|
9
|
+
# # Create a secret key that the hub will use to generate HMAC digests,
|
10
|
+
# # allowing for validation of content delivery payloads.
|
11
|
+
# # NOTE: Must be less than 200 bytes in length.
|
12
|
+
# self.hmac_secret_key = SecureRandom.hex
|
13
|
+
# end
|
14
|
+
|
15
|
+
# Validations
|
16
|
+
validates :hub, presence: true, url: true
|
17
|
+
validates :topic, presence: true, url: true
|
18
|
+
validates :topic_content_type, presence: true
|
19
|
+
validates :hmac_secret_key, presence: true
|
20
|
+
|
21
|
+
# Scopes
|
22
|
+
# scope :expired, -> { where(["expired_at <= ?", Time.now]) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
class ConnectableTest < ActiveSupport::TestCase
|
4
|
+
fixtures :subscriptions
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@subscription = subscriptions(:example)
|
8
|
+
end
|
9
|
+
|
10
|
+
test "callback URI errors when missing config values" do
|
11
|
+
ENV["PUSH_CALLBACK_SCHEME"] = nil
|
12
|
+
assert_raises(Pushcart::ConfigurationError) { @subscription.callback_uri }
|
13
|
+
ENV["PUSH_CALLBACK_SCHEME"] = "http"
|
14
|
+
end
|
15
|
+
|
16
|
+
test "callback URI errors when missing id" do
|
17
|
+
@subscription.id = nil
|
18
|
+
assert_raises(ArgumentError) { @subscription.callback_uri }
|
19
|
+
end
|
20
|
+
|
21
|
+
test "returns an entire callback URL" do
|
22
|
+
assert_equal "http://pubsub.example.com:88/subscriptions/1/callback", @subscription.callback_uri.to_s
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
== README
|
2
|
+
|
3
|
+
This README would normally document whatever steps are necessary to get the
|
4
|
+
application up and running.
|
5
|
+
|
6
|
+
Things you may want to cover:
|
7
|
+
|
8
|
+
* Ruby version
|
9
|
+
|
10
|
+
* System dependencies
|
11
|
+
|
12
|
+
* Configuration
|
13
|
+
|
14
|
+
* Database creation
|
15
|
+
|
16
|
+
* Database initialization
|
17
|
+
|
18
|
+
* How to run the test suite
|
19
|
+
|
20
|
+
* Services (job queues, cache servers, search engines, etc.)
|
21
|
+
|
22
|
+
* Deployment instructions
|
23
|
+
|
24
|
+
* ...
|
25
|
+
|
26
|
+
|
27
|
+
Please feel free to use a different markup language if you do not plan to run
|
28
|
+
<tt>rake doc:app</tt>.
|
data/test/dummy/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any styles
|
10
|
+
* defined in the other CSS/SCSS files in this directory. It is generally better to create a new
|
11
|
+
* file per style scope.
|
12
|
+
*
|
13
|
+
*= require_tree .
|
14
|
+
*= require_self
|
15
|
+
*/
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Subscription < ActiveRecord::Base
|
2
|
+
include Pushcart::Subscribable
|
3
|
+
# before_validation(on: :create) do
|
4
|
+
# # Create a secret key that the hub will use to generate HMAC digests,
|
5
|
+
# # allowing for validation of content delivery payloads.
|
6
|
+
# # NOTE: Must be less than 200 bytes in length.
|
7
|
+
# self.hmac_secret_key = SecureRandom.hex
|
8
|
+
# end
|
9
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Dummy</title>
|
5
|
+
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
|
6
|
+
<%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/test/dummy/bin/rake
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
# path to your application root.
|
5
|
+
APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
|
6
|
+
|
7
|
+
Dir.chdir APP_ROOT do
|
8
|
+
# This script is a starting point to setup your application.
|
9
|
+
# Add necessary setup steps to this file:
|
10
|
+
|
11
|
+
puts "== Installing dependencies =="
|
12
|
+
system "gem install bundler --conservative"
|
13
|
+
system "bundle check || bundle install"
|
14
|
+
|
15
|
+
# puts "\n== Copying sample files =="
|
16
|
+
# unless File.exist?("config/database.yml")
|
17
|
+
# system "cp config/database.yml.sample config/database.yml"
|
18
|
+
# end
|
19
|
+
|
20
|
+
puts "\n== Preparing database =="
|
21
|
+
system "bin/rake db:setup"
|
22
|
+
|
23
|
+
puts "\n== Removing old logs and tempfiles =="
|
24
|
+
system "rm -f log/*"
|
25
|
+
system "rm -rf tmp/cache"
|
26
|
+
|
27
|
+
puts "\n== Restarting application server =="
|
28
|
+
system "touch tmp/restart.txt"
|
29
|
+
end
|