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.
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