tentd 0.0.1
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.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/.travis.yml +8 -0
- data/Gemfile +9 -0
- data/Guardfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +49 -0
- data/Rakefile +8 -0
- data/bin/tent-server +3 -0
- data/lib/tentd.rb +31 -0
- data/lib/tentd/api.rb +58 -0
- data/lib/tentd/api/apps.rb +196 -0
- data/lib/tentd/api/authentication_finalize.rb +12 -0
- data/lib/tentd/api/authentication_lookup.rb +27 -0
- data/lib/tentd/api/authentication_verification.rb +50 -0
- data/lib/tentd/api/authorizable.rb +21 -0
- data/lib/tentd/api/authorization.rb +14 -0
- data/lib/tentd/api/core_profile_data.rb +45 -0
- data/lib/tentd/api/followers.rb +218 -0
- data/lib/tentd/api/followings.rb +241 -0
- data/lib/tentd/api/groups.rb +161 -0
- data/lib/tentd/api/middleware.rb +32 -0
- data/lib/tentd/api/posts.rb +373 -0
- data/lib/tentd/api/profile.rb +78 -0
- data/lib/tentd/api/router.rb +123 -0
- data/lib/tentd/api/router/caching_headers.rb +49 -0
- data/lib/tentd/api/router/extract_params.rb +88 -0
- data/lib/tentd/api/router/serialize_response.rb +38 -0
- data/lib/tentd/api/user_lookup.rb +10 -0
- data/lib/tentd/core_ext/hash/slice.rb +29 -0
- data/lib/tentd/datamapper/array_property.rb +23 -0
- data/lib/tentd/datamapper/query.rb +19 -0
- data/lib/tentd/json_patch.rb +181 -0
- data/lib/tentd/model.rb +30 -0
- data/lib/tentd/model/app.rb +68 -0
- data/lib/tentd/model/app_authorization.rb +113 -0
- data/lib/tentd/model/follower.rb +105 -0
- data/lib/tentd/model/following.rb +100 -0
- data/lib/tentd/model/group.rb +24 -0
- data/lib/tentd/model/mention.rb +19 -0
- data/lib/tentd/model/notification_subscription.rb +56 -0
- data/lib/tentd/model/permissible.rb +227 -0
- data/lib/tentd/model/permission.rb +28 -0
- data/lib/tentd/model/post.rb +178 -0
- data/lib/tentd/model/post_attachment.rb +27 -0
- data/lib/tentd/model/post_version.rb +64 -0
- data/lib/tentd/model/profile_info.rb +80 -0
- data/lib/tentd/model/random_public_id.rb +46 -0
- data/lib/tentd/model/serializable.rb +58 -0
- data/lib/tentd/model/type_properties.rb +36 -0
- data/lib/tentd/model/user.rb +39 -0
- data/lib/tentd/model/user_scoped.rb +14 -0
- data/lib/tentd/notifications.rb +13 -0
- data/lib/tentd/notifications/girl_friday.rb +30 -0
- data/lib/tentd/notifications/sidekiq.rb +50 -0
- data/lib/tentd/tent_type.rb +20 -0
- data/lib/tentd/tent_version.rb +41 -0
- data/lib/tentd/version.rb +3 -0
- data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
- data/spec/fabricators/apps_fabricator.rb +11 -0
- data/spec/fabricators/followers_fabricator.rb +14 -0
- data/spec/fabricators/followings_fabricator.rb +17 -0
- data/spec/fabricators/groups_fabricator.rb +3 -0
- data/spec/fabricators/mentions_fabricator.rb +3 -0
- data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
- data/spec/fabricators/permissions_fabricator.rb +1 -0
- data/spec/fabricators/post_attachments_fabricator.rb +8 -0
- data/spec/fabricators/post_versions_fabricator.rb +12 -0
- data/spec/fabricators/posts_fabricator.rb +12 -0
- data/spec/fabricators/profile_infos_fabricator.rb +30 -0
- data/spec/integration/api/apps_spec.rb +466 -0
- data/spec/integration/api/followers_spec.rb +535 -0
- data/spec/integration/api/followings_spec.rb +688 -0
- data/spec/integration/api/groups_spec.rb +207 -0
- data/spec/integration/api/posts_spec.rb +874 -0
- data/spec/integration/api/profile_spec.rb +285 -0
- data/spec/integration/api/router_spec.rb +102 -0
- data/spec/integration/model/app_authorization_spec.rb +59 -0
- data/spec/integration/model/app_spec.rb +63 -0
- data/spec/integration/model/follower_spec.rb +344 -0
- data/spec/integration/model/following_spec.rb +97 -0
- data/spec/integration/model/group_spec.rb +39 -0
- data/spec/integration/model/notification_subscription_spec.rb +145 -0
- data/spec/integration/model/post_spec.rb +658 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/expect_server.rb +3 -0
- data/spec/support/json_request.rb +54 -0
- data/spec/support/with_constant.rb +23 -0
- data/spec/support/with_warnings.rb +6 -0
- data/spec/unit/api/authentication_finalize_spec.rb +45 -0
- data/spec/unit/api/authentication_lookup_spec.rb +65 -0
- data/spec/unit/api/authentication_verification_spec.rb +50 -0
- data/spec/unit/api/authorizable_spec.rb +50 -0
- data/spec/unit/api/authorization_spec.rb +44 -0
- data/spec/unit/api/caching_headers_spec.rb +121 -0
- data/spec/unit/core_profile_data_spec.rb +64 -0
- data/spec/unit/json_patch_spec.rb +407 -0
- data/spec/unit/tent_type_spec.rb +28 -0
- data/spec/unit/tent_version_spec.rb +68 -0
- data/tentd.gemspec +36 -0
- metadata +435 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module TentD
|
|
2
|
+
module Model
|
|
3
|
+
module TypeProperties
|
|
4
|
+
def self.included(base)
|
|
5
|
+
base.class_eval do
|
|
6
|
+
property :type_base, DataMapper::Property::Text, :required => true, :lazy => false
|
|
7
|
+
property :type_view, String
|
|
8
|
+
property :type_version, String
|
|
9
|
+
|
|
10
|
+
validates_with_block :type_version do
|
|
11
|
+
return true if type_base == 'all' || type_version
|
|
12
|
+
[false, 'type version must be set']
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def type
|
|
18
|
+
TentType.new.tap do |t|
|
|
19
|
+
t.base = type_base
|
|
20
|
+
t.version = type_version
|
|
21
|
+
t.view = type_view
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def type=(new_t)
|
|
26
|
+
if String === new_t
|
|
27
|
+
new_t = TentType.new(new_t)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
self.type_base = new_t.base
|
|
31
|
+
self.type_version = new_t.version
|
|
32
|
+
self.type_view = new_t.view
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module TentD
|
|
2
|
+
module Model
|
|
3
|
+
class User
|
|
4
|
+
include DataMapper::Resource
|
|
5
|
+
|
|
6
|
+
storage_names[:default] = 'users'
|
|
7
|
+
|
|
8
|
+
property :id, Serial
|
|
9
|
+
property :created_at, DateTime
|
|
10
|
+
property :updated_at, DateTime
|
|
11
|
+
property :deleted_at, ParanoidDateTime
|
|
12
|
+
|
|
13
|
+
has n, :posts, 'TentD::Model::Post'
|
|
14
|
+
has n, :post_versions, 'TentD::Model::PostVersion'
|
|
15
|
+
has n, :apps, 'TentD::Model::App'
|
|
16
|
+
has n, :followings, 'TentD::Model::Following'
|
|
17
|
+
has n, :followers, 'TentD::Model::Follower'
|
|
18
|
+
has n, :groups, 'TentD::Model::Group'
|
|
19
|
+
has n, :profile_infos, 'TentD::Model::ProfileInfo'
|
|
20
|
+
has n, :notification_subscriptions, 'TentD::Model::NotificationSubscription'
|
|
21
|
+
|
|
22
|
+
def self.current=(u)
|
|
23
|
+
relationships.each do |relationship|
|
|
24
|
+
relationship.child_model.default_scope(:default).update(:user => u)
|
|
25
|
+
end
|
|
26
|
+
Thread.current[:user] = u
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.current
|
|
30
|
+
Thread.current[:user]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def profile_entity
|
|
34
|
+
info = profile_infos.first(:type_base => ProfileInfo::TENT_PROFILE_TYPE.base, :order => :type_version.desc)
|
|
35
|
+
info.content['entity'] if info
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'tentd/notifications'
|
|
2
|
+
require 'girl_friday'
|
|
3
|
+
|
|
4
|
+
module TentD
|
|
5
|
+
class Notifications
|
|
6
|
+
def self.queue_job(job, msg)
|
|
7
|
+
const_get(job.to_s.upcase+'_QUEUE').push(msg)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
TRIGGER_QUEUE = GirlFriday::WorkQueue.new(:trigger) do |msg|
|
|
11
|
+
Model::NotificationSubscription.notify_all(msg[:type], msg[:post_id])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
NOTIFY_QUEUE = GirlFriday::WorkQueue.new(:notify) do |msg|
|
|
15
|
+
Model::NotificationSubscription.first(:id => msg[:subscription_id]).notify_about(msg[:post_id], msg[:view])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
NOTIFY_ENTITY_QUEUE = GirlFriday::WorkQueue.new(:notify_entity) do |msg|
|
|
19
|
+
Model::NotificationSubscription.notify_entity(msg[:entity], msg[:post_id])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
UPDATE_FOLLOWING_PROFILE_QUEUE = GirlFriday::WorkQueue.new(:update_following_profile) do |msg|
|
|
23
|
+
Model::Following.update_profile(msg[:following_id])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
PROFILE_INFO_UPDATE_QUEUE = GirlFriday::WorkQueue.new(:profile_info_update) do |msg|
|
|
27
|
+
Model::ProfileInfo.create_update_post(msg[:profile_info_id])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require 'tentd/notifications'
|
|
2
|
+
require 'sidekiq'
|
|
3
|
+
|
|
4
|
+
module TentD
|
|
5
|
+
class Notifications
|
|
6
|
+
def self.queue_job(job, msg)
|
|
7
|
+
const_get(job.to_s.split('_').map(&:capitalize).push('Worker').join).perform_async(msg)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class TriggerWorker
|
|
11
|
+
include Sidekiq::Worker
|
|
12
|
+
|
|
13
|
+
def perform(msg)
|
|
14
|
+
Model::NotificationSubscription.notify_all(msg['type'], msg['post_id'])
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class NotifyWorker
|
|
19
|
+
include Sidekiq::Worker
|
|
20
|
+
|
|
21
|
+
def perform(msg)
|
|
22
|
+
Model::NotificationSubscription.first(:id => msg['subscription_id']).notify_about(msg['post_id'])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class NotifyEntityWorker
|
|
27
|
+
include Sidekiq::Worker
|
|
28
|
+
|
|
29
|
+
def perform(msg)
|
|
30
|
+
Model::NotificationSubscription.notify_entity(msg['entity'], msg['post_id'])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class UpdateFollowingProfileWorker
|
|
35
|
+
include Sidekiq::Worker
|
|
36
|
+
|
|
37
|
+
def perform(msg)
|
|
38
|
+
Model::Following.update_profile(msg['following_id'])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class ProfileInfoUpdateWorker
|
|
43
|
+
include Sidekiq::Worker
|
|
44
|
+
|
|
45
|
+
def perform(msg)
|
|
46
|
+
Model::ProfileInfo.create_update_post(msg['profile_info_id'])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module TentD
|
|
2
|
+
class TentType
|
|
3
|
+
attr_accessor :version, :view, :base
|
|
4
|
+
|
|
5
|
+
def initialize(uri = nil)
|
|
6
|
+
if uri
|
|
7
|
+
@version = TentVersion.from_uri(uri)
|
|
8
|
+
view_split = uri.to_s.split('#')
|
|
9
|
+
@view = view_split[1]
|
|
10
|
+
@base = view_split[0].to_s.sub(%r{/v[^a-z/][^/]*$}, '')
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def uri
|
|
15
|
+
version_part = @version.nil? ? '' : "/v#{@version}"
|
|
16
|
+
view_part = @view.nil? ? '' : "##{@view}"
|
|
17
|
+
"#{@base}#{version_part}#{view_part}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module TentD
|
|
2
|
+
class TentVersion
|
|
3
|
+
Infinity = 1 / 0.0
|
|
4
|
+
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
def self.from_uri(uri)
|
|
8
|
+
new((uri.to_s.match(/v([.\dx]+)/) || [])[1])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(version_string)
|
|
12
|
+
@version = version_string
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_s
|
|
16
|
+
@version
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parts
|
|
20
|
+
@version.split('.').map { |p| p == 'x' ? p : p.to_i }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def parts=(array)
|
|
24
|
+
@version = array.join('.')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def <=>(other)
|
|
28
|
+
other = self.class.new(other) if other.kind_of?(String)
|
|
29
|
+
parts.each_with_index.map { |p, index|
|
|
30
|
+
if (p == 'x' || other.parts[index] == 'x') || p == other.parts[index]
|
|
31
|
+
0
|
|
32
|
+
elsif p < other.parts[index]
|
|
33
|
+
-1
|
|
34
|
+
elsif p > other.parts[index]
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
}.each { |r| return r if r != 0 }
|
|
38
|
+
0
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Fabricator(:app, :class_name => 'TentD::Model::App') do
|
|
2
|
+
name "MicroBlogger"
|
|
3
|
+
description "Manages your status updates"
|
|
4
|
+
url "https://microbloggerapp.example.com"
|
|
5
|
+
icon "https://microbloggerapp.example.com/icon.png"
|
|
6
|
+
redirect_uris ["https://microbloggerapp.example.com/auth/callback?foo=bar"]
|
|
7
|
+
scopes { Hash.new(
|
|
8
|
+
"read_posts" => "Can read your posts",
|
|
9
|
+
"create_posts" => "Can create posts on your behalf"
|
|
10
|
+
) }
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Fabricator(:follower, :class_name => "TentD::Model::Follower") do |f|
|
|
2
|
+
f.transient :server_urls
|
|
3
|
+
f.entity "https://smith.example.com"
|
|
4
|
+
f.public true
|
|
5
|
+
f.licenses ["http://creativecommons.org/licenses/by-nc-sa/3.0/", "http://www.gnu.org/copyleft/gpl.html"]
|
|
6
|
+
f.groups { ["family", "friends"].map {|name| Fabricate(:group, :name => name).public_id } }
|
|
7
|
+
f.notification_path "notifications/asdf"
|
|
8
|
+
f.profile { |f|
|
|
9
|
+
{ TentD::Model::ProfileInfo::TENT_PROFILE_TYPE_URI =>
|
|
10
|
+
{ :entity => f[:entity], :licenses => f[:licenses], :servers => Array(f[:server_urls] || ["https://example.com"]) }
|
|
11
|
+
}.to_json
|
|
12
|
+
}
|
|
13
|
+
f.updated_at { Time.now }
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Fabricator(:following, :class_name => "TentD::Model::Following") do
|
|
2
|
+
transient :server_urls
|
|
3
|
+
entity "https://smith.example.com"
|
|
4
|
+
licenses ["http://creativecommons.org/licenses/by-nc-sa/3.0/", "http://www.gnu.org/copyleft/gpl.html"]
|
|
5
|
+
groups { ["family", "friends"].map {|name| Fabricate(:group, :name => name).public_id } }
|
|
6
|
+
mac_key_id { SecureRandom.hex(4) }
|
|
7
|
+
mac_key { SecureRandom.hex(16) }
|
|
8
|
+
mac_algorithm 'hmac-sha-256'
|
|
9
|
+
mac_timestamp_delta Time.now.to_i
|
|
10
|
+
profile { |f|
|
|
11
|
+
{ TentD::Model::ProfileInfo::TENT_PROFILE_TYPE_URI =>
|
|
12
|
+
{ :entity => f[:entity], :licenses => f[:licenses], :servers => Array(f[:server_urls] || ["https://example.com"]) }
|
|
13
|
+
}.to_json
|
|
14
|
+
}
|
|
15
|
+
updated_at { Time.now }
|
|
16
|
+
confirmed true
|
|
17
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Fabricator(:permission, :class_name => "TentD::Model::Permission")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Fabricator(:post_version, :class_name => 'TentD::Model::PostVersion') do |f|
|
|
2
|
+
f.entity "https://smith.example.com"
|
|
3
|
+
f.public true
|
|
4
|
+
f.original true
|
|
5
|
+
f.type_base "https://tent.io/types/post/status"
|
|
6
|
+
f.type_version "0.1.0"
|
|
7
|
+
f.licenses ["http://creativecommons.org/licenses/by-nc-sa/3.0/", "http://www.gnu.org/copyleft/gpl.html"]
|
|
8
|
+
f.content {{ 'text' => "Debitis exercitationem et cum dolores dolor laudantium. Delectus sit eius id. Totam voluptatem et sunt consectetur sed facere debitis. Quia molestias ratione." }}
|
|
9
|
+
f.updated_at { |attrs| Time.now }
|
|
10
|
+
f.published_at { |attrs| Time.now }
|
|
11
|
+
f.received_at { |attrs| Time.now }
|
|
12
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Fabricator(:post, :class_name => "TentD::Model::Post") do |f|
|
|
2
|
+
f.entity "https://smith.example.com"
|
|
3
|
+
f.public true
|
|
4
|
+
f.original true
|
|
5
|
+
f.type_base "https://tent.io/types/post/status"
|
|
6
|
+
f.type_version "0.1.0"
|
|
7
|
+
f.licenses ["http://creativecommons.org/licenses/by-nc-sa/3.0/", "http://www.gnu.org/copyleft/gpl.html"]
|
|
8
|
+
f.content {{ 'text' => "Debitis exercitationem et cum dolores dolor laudantium. Delectus sit eius id. Totam voluptatem et sunt consectetur sed facere debitis. Quia molestias ratione." }}
|
|
9
|
+
f.updated_at { |attrs| Time.now }
|
|
10
|
+
f.published_at { |attrs| Time.now }
|
|
11
|
+
f.received_at { |attrs| Time.now }
|
|
12
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Fabricator(:profile_info, :class_name => 'TentD::Model::ProfileInfo') do |f|
|
|
2
|
+
f.public true
|
|
3
|
+
f.type_base 'https://tent.io/types/info/core'
|
|
4
|
+
f.type_version '0.1.0'
|
|
5
|
+
f.content { |attrs|
|
|
6
|
+
{
|
|
7
|
+
"licenses" => [
|
|
8
|
+
"http://creativecommons.org/licenses/by-nc-sa/3.0/",
|
|
9
|
+
"http://www.gnu.org/copyleft/gpl.html"
|
|
10
|
+
],
|
|
11
|
+
"entity" => attrs[:entity],
|
|
12
|
+
"servers" => [
|
|
13
|
+
attrs[:entity],
|
|
14
|
+
"https://backup-johnsmith.example.com"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
Fabricator(:basic_profile_info, :class_name => 'TentD::Model::ProfileInfo') do |f|
|
|
21
|
+
f.public true
|
|
22
|
+
f.type_base 'https://tent.io/types/info/basic'
|
|
23
|
+
f.type_version '0.1.0'
|
|
24
|
+
f.content {
|
|
25
|
+
{
|
|
26
|
+
"name" => "John Smith",
|
|
27
|
+
"age" => 25
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
end
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'tentd/core_ext/hash/slice'
|
|
3
|
+
|
|
4
|
+
describe TentD::API::Apps do
|
|
5
|
+
def app
|
|
6
|
+
TentD::API.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def authorize!(*scopes)
|
|
10
|
+
env['current_auth'] = stub(
|
|
11
|
+
:kind_of? => true,
|
|
12
|
+
:app_id => nil,
|
|
13
|
+
:id => nil,
|
|
14
|
+
:scopes => scopes
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:env) { Hash.new }
|
|
19
|
+
let(:params) { Hash.new }
|
|
20
|
+
|
|
21
|
+
describe 'GET /apps' do
|
|
22
|
+
context 'when authorized' do
|
|
23
|
+
before { authorize!(:read_apps) }
|
|
24
|
+
|
|
25
|
+
with_mac_key = proc do
|
|
26
|
+
it 'should return list of apps with mac keys' do
|
|
27
|
+
expect(Fabricate(:app)).to be_saved
|
|
28
|
+
|
|
29
|
+
json_get '/apps', params, env
|
|
30
|
+
expect(last_response.status).to eq(200)
|
|
31
|
+
|
|
32
|
+
body = JSON.parse(last_response.body)
|
|
33
|
+
whitelist = %w{ mac_key_id mac_key mac_algorithm }
|
|
34
|
+
body.each { |actual|
|
|
35
|
+
whitelist.each { |key|
|
|
36
|
+
expect(actual).to have_key(key)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
without_mac_key = proc do
|
|
43
|
+
it 'should return list of apps without mac keys' do
|
|
44
|
+
expect(Fabricate(:app)).to be_saved
|
|
45
|
+
|
|
46
|
+
json_get '/apps', params, env
|
|
47
|
+
expect(last_response.status).to eq(200)
|
|
48
|
+
body = JSON.parse(last_response.body)
|
|
49
|
+
blacklist = %w{ mac_key_id mac_key mac_algorithm }
|
|
50
|
+
body.each { |actual|
|
|
51
|
+
blacklist.each { |key|
|
|
52
|
+
expect(actual).to_not have_key(key)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context 'when read_secrets scope authorized' do
|
|
59
|
+
before { authorize!(:read_apps, :read_secrets) }
|
|
60
|
+
context 'with secrets param' do
|
|
61
|
+
before { params['secrets'] = true }
|
|
62
|
+
context '', &with_mac_key
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context 'without secrets param', &without_mac_key
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
context 'when read_secrets scope unauthorized', &without_mac_key
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'when unauthorized' do
|
|
72
|
+
it 'should respond 403' do
|
|
73
|
+
json_get '/apps', params, env
|
|
74
|
+
expect(last_response.status).to eq(403)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
context 'when pretending to be authorized' do
|
|
78
|
+
let(:_app) { Fabricate(:app) }
|
|
79
|
+
before do
|
|
80
|
+
env['current_auth'] = Fabricate(:app_authorization, :app => _app)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'should respond 403' do
|
|
84
|
+
json_get "/apps?app_id=#{ _app.public_id }", params, env
|
|
85
|
+
expect(last_response.status).to eq(403)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe 'GET /apps/:id' do
|
|
92
|
+
without_mac_key = proc do
|
|
93
|
+
it 'should return app without mac_key' do
|
|
94
|
+
app = _app
|
|
95
|
+
|
|
96
|
+
json_get "/apps/#{app.public_id}", params, env
|
|
97
|
+
expect(last_response.status).to eq(200)
|
|
98
|
+
body = JSON.parse(last_response.body)
|
|
99
|
+
blacklist = %w{ mac_key_id mac_key mac_algorithm }
|
|
100
|
+
blacklist.each { |key|
|
|
101
|
+
expect(body).to_not have_key(key)
|
|
102
|
+
}
|
|
103
|
+
expect(body['id']).to eq(app.public_id)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
context 'when authorized via scope' do
|
|
108
|
+
let(:_app) { Fabricate(:app) }
|
|
109
|
+
before { authorize!(:read_apps) }
|
|
110
|
+
|
|
111
|
+
context 'app with :id exists' do
|
|
112
|
+
context 'when read_secrets scope authorized' do
|
|
113
|
+
before { authorize!(:read_apps, :read_secrets) }
|
|
114
|
+
|
|
115
|
+
context 'with read secrets param' do
|
|
116
|
+
before { params['secrets'] = true }
|
|
117
|
+
|
|
118
|
+
it 'should return app with mac_key' do
|
|
119
|
+
app = _app
|
|
120
|
+
json_get "/apps/#{app.public_id}", params, env
|
|
121
|
+
expect(last_response.status).to eq(200)
|
|
122
|
+
body = JSON.parse(last_response.body)
|
|
123
|
+
whitelist = %w{ mac_key_id mac_key mac_algorithm }
|
|
124
|
+
whitelist.each { |key|
|
|
125
|
+
expect(body).to have_key(key)
|
|
126
|
+
}
|
|
127
|
+
expect(body['id']).to eq(app.public_id)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
context 'without read secrets param', &without_mac_key
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
context 'when read_secrets scope unauthorized', &without_mac_key
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'app with :id does not exist' do
|
|
138
|
+
it 'should return 404' do
|
|
139
|
+
json_get "/apps/app-id", params, env
|
|
140
|
+
expect(last_response.status).to eq(404)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'when authorized via identity' do
|
|
146
|
+
let(:_app) { Fabricate(:app) }
|
|
147
|
+
examples = proc do
|
|
148
|
+
context 'app with :id exists' do
|
|
149
|
+
context 'without secrets param', &without_mac_key
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
context 'app with :id does not exist' do
|
|
153
|
+
it 'should return 403' do
|
|
154
|
+
json_get '/apps/app-id', params, env
|
|
155
|
+
expect(last_response.status).to eq(403)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
context 'when App' do
|
|
161
|
+
before do
|
|
162
|
+
env['current_auth'] = _app
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
context &examples
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
context 'when unauthorized' do
|
|
170
|
+
it 'should respond 403' do
|
|
171
|
+
json_get "/apps/app-id", params, env
|
|
172
|
+
expect(last_response.status).to eq(403)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
describe 'POST /apps' do
|
|
178
|
+
it 'should create app' do
|
|
179
|
+
data = Fabricate.build(:app).attributes.slice(:name, :description, :url, :icon, :redirect_uris, :scopes)
|
|
180
|
+
|
|
181
|
+
TentD::Model::App.all.destroy
|
|
182
|
+
expect(lambda { json_post '/apps', data, env }).to change(TentD::Model::App, :count).by(1)
|
|
183
|
+
|
|
184
|
+
app = TentD::Model::App.last
|
|
185
|
+
expect(last_response.status).to eq(200)
|
|
186
|
+
body = JSON.parse(last_response.body)
|
|
187
|
+
whitelist = %w{ mac_key_id mac_key mac_algorithm }
|
|
188
|
+
whitelist.each { |key|
|
|
189
|
+
expect(body).to have_key(key)
|
|
190
|
+
}
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
describe 'POST /apps/:id/authorizations' do
|
|
195
|
+
context 'when authorized' do
|
|
196
|
+
before { authorize!(:write_apps) }
|
|
197
|
+
|
|
198
|
+
it 'should create app authorization' do
|
|
199
|
+
app = Fabricate(:app)
|
|
200
|
+
scopes = %w{ read_posts write_posts }
|
|
201
|
+
post_types = %w{ https://tent.io/types/post/status/v0.1.0 https://tent.io/types/post/photo/v0.1.0 }
|
|
202
|
+
profile_info_types = %w{ https://tent.io/types/info/basic/v0.1.0 https://tent.io/types/info/core/v0.1.0 }
|
|
203
|
+
data = {
|
|
204
|
+
:notification_url => "http://example.com/webhooks/notifications",
|
|
205
|
+
:scopes => scopes,
|
|
206
|
+
:post_types => post_types.map {|url| URI.encode(url, ":/") },
|
|
207
|
+
:profile_info_types => profile_info_types.map {|url| URI.encode(url, ":/") },
|
|
208
|
+
}
|
|
209
|
+
expect(lambda {
|
|
210
|
+
expect(lambda {
|
|
211
|
+
json_post "/apps/#{app.public_id}/authorizations", data, env
|
|
212
|
+
expect(last_response.status).to eq(200)
|
|
213
|
+
}).to change(TentD::Model::NotificationSubscription, :count).by(2)
|
|
214
|
+
}).to change(TentD::Model::AppAuthorization, :count)
|
|
215
|
+
|
|
216
|
+
app_auth = app.authorizations.last
|
|
217
|
+
expect(app_auth.scopes).to eq(scopes)
|
|
218
|
+
expect(app_auth.post_types).to eq(post_types)
|
|
219
|
+
expect(app_auth.profile_info_types).to eq(profile_info_types)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
context 'when not authorized' do
|
|
224
|
+
context 'when token exchange' do
|
|
225
|
+
it 'should exchange mac_key_id for mac_key' do
|
|
226
|
+
app = Fabricate(:app)
|
|
227
|
+
authorization = app.authorizations.create
|
|
228
|
+
|
|
229
|
+
data = {
|
|
230
|
+
:code => authorization.token_code
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
json_post "/apps/#{app.public_id}/authorizations", data, env
|
|
234
|
+
expect(last_response.status).to eq(200)
|
|
235
|
+
expect(authorization.reload.token_code).to_not eq(data[:code])
|
|
236
|
+
body = JSON.parse(last_response.body)
|
|
237
|
+
whitelist = %w{ access_token mac_key mac_algorithm token_type }
|
|
238
|
+
whitelist.each { |key|
|
|
239
|
+
expect(body).to have_key(key)
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'should return 403' do
|
|
245
|
+
app = Fabricate(:app)
|
|
246
|
+
expect(lambda {
|
|
247
|
+
json_post "/apps/#{app.public_id}/authorizations", params, env
|
|
248
|
+
}).to_not change(TentD::Model::AppAuthorization, :count)
|
|
249
|
+
expect(last_response.status).to eq(403)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
describe 'PUT /apps/:id' do
|
|
255
|
+
authorized_examples = proc do
|
|
256
|
+
context 'app with :id exists' do
|
|
257
|
+
it 'should update app' do
|
|
258
|
+
app = _app
|
|
259
|
+
data = app.attributes.slice(:name, :url, :icon, :redirect_uris, :scopes)
|
|
260
|
+
data[:name] = "Yet Another MicroBlog App"
|
|
261
|
+
data[:scopes] = {
|
|
262
|
+
"read_posts" => "Can read your posts"
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
json_put "/apps/#{app.public_id}", data, env
|
|
266
|
+
expect(last_response.status).to eq(200)
|
|
267
|
+
app.reload
|
|
268
|
+
data.slice(:name, :scopes, :url, :icon, :redirect_uris).each_pair do |key, val|
|
|
269
|
+
expect(app.send(key).to_json).to eq(val.to_json)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
context 'when authorized via scope' do
|
|
276
|
+
let(:_app) { Fabricate(:app) }
|
|
277
|
+
before { authorize!(:write_apps) }
|
|
278
|
+
|
|
279
|
+
context '', &authorized_examples
|
|
280
|
+
|
|
281
|
+
context 'app with :id does not exist' do
|
|
282
|
+
it 'should return 404' do
|
|
283
|
+
json_put "/apps/#{(TentD::Model::App.count + 1) * 100}", params, env
|
|
284
|
+
expect(last_response.status).to eq(404)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
context 'when authorized as app' do
|
|
290
|
+
let(:_app) { Fabricate(:app) }
|
|
291
|
+
|
|
292
|
+
before do
|
|
293
|
+
env['current_auth'] = _app
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
context '', &authorized_examples
|
|
297
|
+
|
|
298
|
+
context 'app with :id does not exist' do
|
|
299
|
+
it 'should return 403' do
|
|
300
|
+
json_put "/apps/app-id", params, env
|
|
301
|
+
expect(last_response.status).to eq(403)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
context 'when unauthorized' do
|
|
307
|
+
it 'should respond 403' do
|
|
308
|
+
json_put '/apps/app-id', params, env
|
|
309
|
+
expect(last_response.status).to eq(403)
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
describe 'DELETE /apps/:id' do
|
|
315
|
+
authorized_examples = proc do
|
|
316
|
+
context 'app with :id exists' do
|
|
317
|
+
it 'should delete app' do
|
|
318
|
+
app = _app
|
|
319
|
+
expect(app).to be_saved
|
|
320
|
+
|
|
321
|
+
expect(lambda {
|
|
322
|
+
delete "/apps/#{app.public_id}", params, env
|
|
323
|
+
expect(last_response.status).to eq(200)
|
|
324
|
+
}).to change(TentD::Model::App, :count).by(-1)
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
context 'when authorized via scope' do
|
|
330
|
+
before { authorize!(:write_apps) }
|
|
331
|
+
let(:_app) { Fabricate(:app) }
|
|
332
|
+
|
|
333
|
+
context '', &authorized_examples
|
|
334
|
+
context 'app with :id does not exist' do
|
|
335
|
+
it 'should return 404' do
|
|
336
|
+
delete "/apps/app-id", params, env
|
|
337
|
+
expect(last_response.status).to eq(404)
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
context 'when authorized via identity' do
|
|
343
|
+
let(:_app) { Fabricate(:app) }
|
|
344
|
+
before do
|
|
345
|
+
env['current_auth'] = _app
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
context '', &authorized_examples
|
|
349
|
+
|
|
350
|
+
context 'app with :id does not exist' do
|
|
351
|
+
it 'should respond 403' do
|
|
352
|
+
delete '/apps/app-id', params, env
|
|
353
|
+
expect(last_response.status).to eq(403)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
context 'when unauthorized' do
|
|
359
|
+
it 'should respond 403' do
|
|
360
|
+
delete '/apps/app-id', params, env
|
|
361
|
+
expect(last_response.status).to eq(403)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
describe 'PUT /apps/:app_id/authorizations/:auth_id' do
|
|
367
|
+
let!(:_app) { Fabricate(:app) }
|
|
368
|
+
let!(:app_auth) { Fabricate(:app_authorization, :post_types => [], :profile_info_types => [], :notification_url => "http://example.com/notification", :app => _app) }
|
|
369
|
+
|
|
370
|
+
context 'when authorized via scope' do
|
|
371
|
+
before { authorize!(:write_apps) }
|
|
372
|
+
|
|
373
|
+
context 'update params unrelated to notification subscription' do
|
|
374
|
+
it 'should update app authorization' do
|
|
375
|
+
data = {
|
|
376
|
+
:notification_url => "http://example.com/webhooks/notifications",
|
|
377
|
+
:profile_info_types => ["https://tent.io/types/info/basic/v0.1.0"],
|
|
378
|
+
:scopes => %w{ read_posts read_apps }
|
|
379
|
+
}
|
|
380
|
+
json_put "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", data, env
|
|
381
|
+
expect(last_response.status).to eq(200)
|
|
382
|
+
|
|
383
|
+
app_auth.reload
|
|
384
|
+
data.each_pair do |key, val|
|
|
385
|
+
expect(app_auth.send(key)).to eq(data[key])
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
context 'update post_types' do
|
|
391
|
+
it 'should update notification subscriptions' do
|
|
392
|
+
data = {
|
|
393
|
+
:post_types => ["https://tent.io/types/post/status/v0.1.0"]
|
|
394
|
+
}
|
|
395
|
+
expect(lambda {
|
|
396
|
+
json_put "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", data, env
|
|
397
|
+
expect(last_response.status).to eq(200)
|
|
398
|
+
}).to change(TentD::Model::NotificationSubscription, :count).by(1)
|
|
399
|
+
|
|
400
|
+
expect(lambda {
|
|
401
|
+
json_put "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", data, env
|
|
402
|
+
expect(last_response.status).to eq(200)
|
|
403
|
+
}).to_not change(TentD::Model::NotificationSubscription, :count)
|
|
404
|
+
|
|
405
|
+
app_auth.reload
|
|
406
|
+
expect(app_auth.post_types).to eq(data[:post_types])
|
|
407
|
+
|
|
408
|
+
expect(lambda {
|
|
409
|
+
data[:post_types] = []
|
|
410
|
+
json_put "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", data, env
|
|
411
|
+
expect(last_response.status).to eq(200)
|
|
412
|
+
}).to change(TentD::Model::NotificationSubscription, :count).by(-1)
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it 'should return 404 unless app and authorization exist' do
|
|
417
|
+
json_put "/apps/app-id/authorizations/auth-id", params, env
|
|
418
|
+
expect(last_response.status).to eq(404)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
describe 'DELETE /apps/:app_id/authorizations/:auth_id' do
|
|
424
|
+
let!(:_app) { Fabricate(:app) }
|
|
425
|
+
let!(:app_auth) { Fabricate(:app_authorization, :app => _app) }
|
|
426
|
+
context 'when authorized via scope' do
|
|
427
|
+
before { authorize!(:write_apps) }
|
|
428
|
+
|
|
429
|
+
it 'should delete app authorization' do
|
|
430
|
+
expect(lambda {
|
|
431
|
+
expect(lambda {
|
|
432
|
+
delete "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", params, env
|
|
433
|
+
}).to_not change(TentD::Model::App, :count)
|
|
434
|
+
expect(last_response.status).to eq(200)
|
|
435
|
+
}).to change(TentD::Model::AppAuthorization, :count).by(-1)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
it 'should return 404 unless app and authorization exist' do
|
|
439
|
+
expect(lambda {
|
|
440
|
+
expect(lambda {
|
|
441
|
+
delete "/apps/app-id/authorizations/#{app_auth.public_id}", params, env
|
|
442
|
+
expect(last_response.status).to eq(404)
|
|
443
|
+
}).to_not change(TentD::Model::App, :count)
|
|
444
|
+
}).to_not change(TentD::Model::AppAuthorization, :count)
|
|
445
|
+
|
|
446
|
+
expect(lambda {
|
|
447
|
+
expect(lambda {
|
|
448
|
+
delete "/apps/#{_app.public_id}/authorizations/auth-id", params, env
|
|
449
|
+
expect(last_response.status).to eq(404)
|
|
450
|
+
}).to_not change(TentD::Model::App, :count)
|
|
451
|
+
}).to_not change(TentD::Model::AppAuthorization, :count)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
context 'when not authorized' do
|
|
456
|
+
it 'it should return 403' do
|
|
457
|
+
expect(lambda {
|
|
458
|
+
expect(lambda {
|
|
459
|
+
delete "/apps/#{_app.public_id}/authorizations/#{app_auth.public_id}", params, env
|
|
460
|
+
}).to_not change(TentD::Model::App, :count)
|
|
461
|
+
expect(last_response.status).to eq(403)
|
|
462
|
+
}).to_not change(TentD::Model::AppAuthorization, :count)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|