tentd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/.travis.yml +8 -0
  4. data/Gemfile +9 -0
  5. data/Guardfile +6 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +49 -0
  8. data/Rakefile +8 -0
  9. data/bin/tent-server +3 -0
  10. data/lib/tentd.rb +31 -0
  11. data/lib/tentd/api.rb +58 -0
  12. data/lib/tentd/api/apps.rb +196 -0
  13. data/lib/tentd/api/authentication_finalize.rb +12 -0
  14. data/lib/tentd/api/authentication_lookup.rb +27 -0
  15. data/lib/tentd/api/authentication_verification.rb +50 -0
  16. data/lib/tentd/api/authorizable.rb +21 -0
  17. data/lib/tentd/api/authorization.rb +14 -0
  18. data/lib/tentd/api/core_profile_data.rb +45 -0
  19. data/lib/tentd/api/followers.rb +218 -0
  20. data/lib/tentd/api/followings.rb +241 -0
  21. data/lib/tentd/api/groups.rb +161 -0
  22. data/lib/tentd/api/middleware.rb +32 -0
  23. data/lib/tentd/api/posts.rb +373 -0
  24. data/lib/tentd/api/profile.rb +78 -0
  25. data/lib/tentd/api/router.rb +123 -0
  26. data/lib/tentd/api/router/caching_headers.rb +49 -0
  27. data/lib/tentd/api/router/extract_params.rb +88 -0
  28. data/lib/tentd/api/router/serialize_response.rb +38 -0
  29. data/lib/tentd/api/user_lookup.rb +10 -0
  30. data/lib/tentd/core_ext/hash/slice.rb +29 -0
  31. data/lib/tentd/datamapper/array_property.rb +23 -0
  32. data/lib/tentd/datamapper/query.rb +19 -0
  33. data/lib/tentd/json_patch.rb +181 -0
  34. data/lib/tentd/model.rb +30 -0
  35. data/lib/tentd/model/app.rb +68 -0
  36. data/lib/tentd/model/app_authorization.rb +113 -0
  37. data/lib/tentd/model/follower.rb +105 -0
  38. data/lib/tentd/model/following.rb +100 -0
  39. data/lib/tentd/model/group.rb +24 -0
  40. data/lib/tentd/model/mention.rb +19 -0
  41. data/lib/tentd/model/notification_subscription.rb +56 -0
  42. data/lib/tentd/model/permissible.rb +227 -0
  43. data/lib/tentd/model/permission.rb +28 -0
  44. data/lib/tentd/model/post.rb +178 -0
  45. data/lib/tentd/model/post_attachment.rb +27 -0
  46. data/lib/tentd/model/post_version.rb +64 -0
  47. data/lib/tentd/model/profile_info.rb +80 -0
  48. data/lib/tentd/model/random_public_id.rb +46 -0
  49. data/lib/tentd/model/serializable.rb +58 -0
  50. data/lib/tentd/model/type_properties.rb +36 -0
  51. data/lib/tentd/model/user.rb +39 -0
  52. data/lib/tentd/model/user_scoped.rb +14 -0
  53. data/lib/tentd/notifications.rb +13 -0
  54. data/lib/tentd/notifications/girl_friday.rb +30 -0
  55. data/lib/tentd/notifications/sidekiq.rb +50 -0
  56. data/lib/tentd/tent_type.rb +20 -0
  57. data/lib/tentd/tent_version.rb +41 -0
  58. data/lib/tentd/version.rb +3 -0
  59. data/spec/fabricators/app_authorizations_fabricator.rb +5 -0
  60. data/spec/fabricators/apps_fabricator.rb +11 -0
  61. data/spec/fabricators/followers_fabricator.rb +14 -0
  62. data/spec/fabricators/followings_fabricator.rb +17 -0
  63. data/spec/fabricators/groups_fabricator.rb +3 -0
  64. data/spec/fabricators/mentions_fabricator.rb +3 -0
  65. data/spec/fabricators/notification_subscriptions_fabricator.rb +4 -0
  66. data/spec/fabricators/permissions_fabricator.rb +1 -0
  67. data/spec/fabricators/post_attachments_fabricator.rb +8 -0
  68. data/spec/fabricators/post_versions_fabricator.rb +12 -0
  69. data/spec/fabricators/posts_fabricator.rb +12 -0
  70. data/spec/fabricators/profile_infos_fabricator.rb +30 -0
  71. data/spec/integration/api/apps_spec.rb +466 -0
  72. data/spec/integration/api/followers_spec.rb +535 -0
  73. data/spec/integration/api/followings_spec.rb +688 -0
  74. data/spec/integration/api/groups_spec.rb +207 -0
  75. data/spec/integration/api/posts_spec.rb +874 -0
  76. data/spec/integration/api/profile_spec.rb +285 -0
  77. data/spec/integration/api/router_spec.rb +102 -0
  78. data/spec/integration/model/app_authorization_spec.rb +59 -0
  79. data/spec/integration/model/app_spec.rb +63 -0
  80. data/spec/integration/model/follower_spec.rb +344 -0
  81. data/spec/integration/model/following_spec.rb +97 -0
  82. data/spec/integration/model/group_spec.rb +39 -0
  83. data/spec/integration/model/notification_subscription_spec.rb +145 -0
  84. data/spec/integration/model/post_spec.rb +658 -0
  85. data/spec/spec_helper.rb +37 -0
  86. data/spec/support/expect_server.rb +3 -0
  87. data/spec/support/json_request.rb +54 -0
  88. data/spec/support/with_constant.rb +23 -0
  89. data/spec/support/with_warnings.rb +6 -0
  90. data/spec/unit/api/authentication_finalize_spec.rb +45 -0
  91. data/spec/unit/api/authentication_lookup_spec.rb +65 -0
  92. data/spec/unit/api/authentication_verification_spec.rb +50 -0
  93. data/spec/unit/api/authorizable_spec.rb +50 -0
  94. data/spec/unit/api/authorization_spec.rb +44 -0
  95. data/spec/unit/api/caching_headers_spec.rb +121 -0
  96. data/spec/unit/core_profile_data_spec.rb +64 -0
  97. data/spec/unit/json_patch_spec.rb +407 -0
  98. data/spec/unit/tent_type_spec.rb +28 -0
  99. data/spec/unit/tent_version_spec.rb +68 -0
  100. data/tentd.gemspec +36 -0
  101. 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,14 @@
1
+ module TentD
2
+ module Model
3
+ module UserScoped
4
+ def self.included(base)
5
+ base.class_eval do
6
+ belongs_to :user, 'TentD::Model::User'
7
+ before :valid? do
8
+ self.user_id ||= User.current.id if User.current
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module TentD
2
+ class Notifications
3
+ # current job types
4
+ # - trigger
5
+ # - notify
6
+ # - notify_entity
7
+ # - update_following_profile
8
+ # - profile_info_update
9
+ def self.method_missing(*args)
10
+ send(:queue_job, *args)
11
+ end
12
+ end
13
+ 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,3 @@
1
+ module TentD
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,5 @@
1
+ Fabricator(:app_authorization, :class_name => 'TentD::Model::AppAuthorization') do |f|
2
+ f.notification_url "http://example.com/notifications"
3
+ updated_at { Time.now }
4
+ created_at { Time.now }
5
+ 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,3 @@
1
+ Fabricator(:group, :class_name => "TentD::Model::Group") do |f|
2
+ f.name 'Foos'
3
+ end
@@ -0,0 +1,3 @@
1
+ Fabricator(:mention, :class_name => 'TentD::Model::Mention') do
2
+ entity 'https://johnsmith.example.org'
3
+ end
@@ -0,0 +1,4 @@
1
+ Fabricator(:notification_subscription, :class_name => 'TentD::Model::NotificationSubscription') do |f|
2
+ f.type_base 'https://tent.io/types/post/status'
3
+ f.type_version '0.1.0'
4
+ end
@@ -0,0 +1 @@
1
+ Fabricator(:permission, :class_name => "TentD::Model::Permission")
@@ -0,0 +1,8 @@
1
+ Fabricator(:post_attachment, :class_name => "TentD::Model::PostAttachment") do |f|
2
+ post
3
+ f.category 'foo-category'
4
+ f.type 'text/plain'
5
+ f.name 'asdf.txt'
6
+ f.data "NTQzMjE=\n"
7
+ f.size 5
8
+ end
@@ -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