jsonapi-resources 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.
@@ -0,0 +1,59 @@
1
+ module Helpers
2
+ module FunctionalHelpers
3
+ # from http://jamieonsoftware.com/blog/entry/testing-restful-response-types
4
+ # def assert_response_is(type, message = '')
5
+ # case type
6
+ # when :js
7
+ # check = [
8
+ # 'text/javascript'
9
+ # ]
10
+ # when :json
11
+ # check = [
12
+ # 'application/json',
13
+ # 'text/json',
14
+ # 'application/x-javascript',
15
+ # 'text/x-javascript',
16
+ # 'text/x-json'
17
+ # ]
18
+ # when :xml
19
+ # check = [ 'application/xml', 'text/xml' ]
20
+ # when :yaml
21
+ # check = [
22
+ # 'text/yaml',
23
+ # 'text/x-yaml',
24
+ # 'application/yaml',
25
+ # 'application/x-yaml'
26
+ # ]
27
+ # else
28
+ # if methods.include?('assert_response_types')
29
+ # check = assert_response_types
30
+ # else
31
+ # check = []
32
+ # end
33
+ # end
34
+ #
35
+ # if @response.content_type
36
+ # ct = @response.content_type
37
+ # elsif methods.include?('assert_response_response')
38
+ # ct = assert_response_response
39
+ # else
40
+ # ct = ''
41
+ # end
42
+ #
43
+ # begin
44
+ # assert check.include?(ct)
45
+ # rescue Test::Unit::AssertionFailedError
46
+ # raise Test::Unit::AssertionFailedError.new(build_message(message, "The response type is not ?", type.to_s))
47
+ # end
48
+ # end
49
+
50
+ # def assert_js_redirect_to(path)
51
+ # assert_response_is :js
52
+ # assert_match /#{"window.location.href = \"" + path + "\""}/, @response.body
53
+ # end
54
+ #
55
+ def json_response
56
+ JSON.parse(@response.body)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,13 @@
1
+ module Helpers
2
+ module HashHelpers
3
+ def assert_hash_contains(exp, act, msg = nil)
4
+ msg = message(msg, '') { diff exp, act }
5
+ assert(matches_hash?(exp, act), msg)
6
+ end
7
+
8
+ def assert_hash_equals(exp, act, msg = nil)
9
+ msg = message(msg, '') { diff exp, act }
10
+ assert(matches_hash?(exp, act, {exact: true}), msg)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,60 @@
1
+ module Helpers
2
+ module ValueMatchers
3
+ ### Matchers
4
+ def matches_value?(v1, v2, options = {})
5
+ if v1 == :any
6
+ # any value is acceptable
7
+ elsif v1 == :not_nil
8
+ return false if v2 == nil
9
+ elsif v1.kind_of?(Hash)
10
+ return false unless matches_hash?(v1, v2, options)
11
+ elsif v1.kind_of?(Array)
12
+ return false unless matches_array?(v1, v2, options)
13
+ else
14
+ return false unless v2 == v1
15
+ end
16
+ true
17
+ end
18
+
19
+ def matches_array?(array1, array2, options = {})
20
+ return false unless array1.kind_of?(Array) && array2.kind_of?(Array)
21
+ if options[:exact]
22
+ return false unless array1.size == array2.size
23
+ end
24
+
25
+ # order of items shouldn't matter:
26
+ # ['a', 'b', 'c'], ['b', 'c', 'a'] -> true
27
+ #
28
+ # matched items should only be used once:
29
+ # ['a', 'b', 'c'], ['a', 'a', 'a'] -> false
30
+ # ['a', 'a', 'a'], ['a', 'b', 'c'] -> false
31
+ matched = {}
32
+ (0..(array1.size - 1)).each do |i|
33
+ (0..(array2.size - 1)).each do |j|
34
+ if !matched.has_value?(j.to_s) && matches_value?(array1[i], array2[j], options)
35
+ matched[i.to_s] = j.to_s
36
+ break
37
+ end
38
+ end
39
+ return false unless matched.has_key?(i.to_s)
40
+ end
41
+ true
42
+ end
43
+
44
+ # options => {exact: true} # hashes must match exactly (i.e. have same number of key-value pairs that are all equal)
45
+ def matches_hash?(hash1, hash2, options = {})
46
+ return false unless hash1.kind_of?(Hash) && hash2.kind_of?(Hash)
47
+ if options[:exact]
48
+ return false unless hash1.size == hash2.size
49
+ end
50
+
51
+ hash1 = hash1.deep_symbolize_keys
52
+ hash2 = hash2.deep_symbolize_keys
53
+
54
+ hash1.each do |k1, v1|
55
+ return false unless hash2.has_key?(k1) && matches_value?(v1, hash2[k1], options)
56
+ end
57
+ true
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,40 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ class ValueMatchersTest < ActionController::TestCase
4
+
5
+ def test_matches_value_any
6
+ assert(matches_value?(:any, 'a'))
7
+ assert(matches_value?(:any, nil))
8
+ end
9
+
10
+ def test_matches_value_not_nil
11
+ assert(matches_value?(:not_nil, 'a'))
12
+ refute(matches_value?(:not_nil, nil))
13
+ end
14
+
15
+ def test_matches_value_array
16
+ assert(matches_value?(['a', 'b', 'c'], ['b', 'c', 'a']))
17
+ assert(matches_value?(['a', 'b', 'c'], ['a', 'b', 'c']))
18
+ refute(matches_value?(['a', 'b', 'c'], ['a', 'a']))
19
+ refute(matches_value?(['a', 'b', 'c'], ['a', 'b', 'd']))
20
+
21
+ assert(matches_value?(['a', 'b', :any], ['a', 'b', 'c']))
22
+ assert(matches_value?(['a', 'b', :not_nil], ['a', 'b', 'c']))
23
+ refute(matches_value?(['a', 'b', :not_nil], ['a', 'b', nil]))
24
+ end
25
+
26
+ def test_matches_value_hash
27
+ assert(matches_value?({a: 'a', b: 'b', c: 'c'}, {a: 'a', b: 'b', c: 'c'}))
28
+ assert(matches_value?({a: 'a', b: 'b', c: 'c'}, {b: 'b', c: 'c', a: 'a'}))
29
+ refute(matches_value?({a: 'a', b: 'b', c: 'c'}, {b: 'a', c: 'c', a: 'b'}))
30
+
31
+ assert(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: 'e'}}, {b: 'b', c: {a: 'a', d: 'e'}, a: 'a'}))
32
+ refute(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: 'd'}}, {b: 'b', c: {a: 'a', d: 'e'}, a: 'a'}))
33
+
34
+ assert(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: {a: :not_nil}}}, {b: 'b', c: {a: 'a', d: {a: 'b'}}, a: 'a'}))
35
+ refute(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: {a: :not_nil}}}, {b: 'b', c: {a: 'a', d: {a: nil}}, a: 'a'}))
36
+
37
+ assert(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: {a: :any}}}, {b: 'b', c: {a: 'a', d: {a: 'b'}}, a: 'a'}))
38
+ assert(matches_value?({a: 'a', b: 'b', c: {a: 'a', d: {a: :any}}}, {b: 'b', c: {a: 'a', d: {a: nil}}, a: 'a'}))
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+ require File.expand_path('../../../fixtures/active_record', __FILE__)
3
+
4
+ class RequestTest < ActionDispatch::IntegrationTest
5
+
6
+ def test_get
7
+ get '/posts'
8
+ assert_equal 200, status
9
+ end
10
+
11
+ def test_put_single
12
+ put '/posts/3', {"posts" => {"id" => "3", "title" => "A great new Post", "links" => { "tags" => [3,4] }}}
13
+ assert_equal 200, status
14
+ end
15
+
16
+ # def test_put_links
17
+ # put '/posts/3/links/tags', {"tags" => [1,4] }
18
+ # assert_equal 200, status
19
+ # end
20
+
21
+ # def test_patch_create
22
+ # patch '/posts',
23
+ # {"op" => "add",
24
+ # "path" => "/-",
25
+ # "value" => {"title" => "Another great new Post", "body" => "saasd", "links" => { "author" => 3 }}}
26
+ # assert_equal 200, status
27
+ # end
28
+
29
+ def test_destroy_single
30
+ delete '/posts/7'
31
+ assert_equal 204, status
32
+ end
33
+
34
+ def test_destroy_multiple
35
+ delete '/posts/8,9'
36
+ assert_equal 204, status
37
+ end
38
+ end
39
+
@@ -0,0 +1,85 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+
3
+ class RoutesTest < ActionDispatch::IntegrationTest
4
+
5
+ def test_routing_post
6
+ assert_routing({ path: 'posts', method: :post }, { controller: 'posts', action: 'create' })
7
+ end
8
+
9
+ def test_routing_put
10
+ assert_routing({ path: '/posts/1', method: :put }, { controller: 'posts', action: 'update', id: '1' })
11
+ end
12
+
13
+ def test_routing_posts_show
14
+ assert_routing({ path: '/posts/1', method: :get }, {action: 'show', controller: 'posts', id: '1'})
15
+ end
16
+
17
+ def test_routing_posts_links_author_show
18
+ assert_routing({ path: '/posts/1/links/author', method: :get }, { controller: 'posts', action: 'show_association', post_id: '1', association: 'author' })
19
+ end
20
+
21
+ def test_routing_posts_links_author_destroy
22
+ assert_routing({ path: '/posts/1/links/author', method: :delete }, { controller: 'posts', action: 'destroy_association', post_id: '1', association: 'author' })
23
+ end
24
+
25
+ def test_routing_posts_links_author_create
26
+ assert_routing({ path: '/posts/1/links/author', method: :post }, { controller: 'posts', action: 'create_association', post_id: '1', association: 'author' })
27
+ end
28
+
29
+ def test_routing_posts_links_tags_show
30
+ assert_routing({ path: '/posts/1/links/tags', method: :get }, { controller: 'posts', action: 'show_association', post_id: '1', association: 'tags' })
31
+ end
32
+
33
+ def test_routing_posts_links_tags_destroy
34
+ assert_routing({ path: '/posts/1/links/tags/1,2', method: :delete }, { controller: 'posts', action: 'destroy_association', post_id: '1', keys: '1,2', association: 'tags' })
35
+ end
36
+
37
+ def test_routing_posts_links_tags_update
38
+ assert_routing({ path: '/posts/1/links/tags', method: :post }, { controller: 'posts', action: 'create_association', post_id: '1', association: 'tags' })
39
+ end
40
+
41
+ # V1
42
+ def test_routing_v1_posts_show
43
+ assert_routing({ path: '/api/v1/posts/1', method: :get }, {action: 'show', controller: 'api/v1/posts', id: '1'})
44
+ end
45
+
46
+ def test_routing_v1_posts_delete
47
+ assert_routing({ path: '/api/v1/posts/1', method: :delete }, {action: 'destroy', controller: 'api/v1/posts', id: '1'})
48
+ end
49
+
50
+ def test_routing_v1_posts_links_author_show
51
+ assert_routing({ path: '/api/v1/posts/1/links/author', method: :get }, { controller: 'api/v1/posts', action: 'show_association', post_id: '1', association: 'author' })
52
+ end
53
+
54
+ # V2
55
+ def test_routing_v2_posts_show
56
+ assert_routing({ path: '/api/v2/authors/1', method: :get }, {action: 'show', controller: 'api/v2/authors', id: '1'})
57
+ end
58
+
59
+ def test_routing_v2_posts_links_author_show
60
+ assert_routing({ path: '/api/v2/posts/1/links/author', method: :get }, { controller: 'api/v2/posts', action: 'show_association', post_id: '1', association: 'author' })
61
+ end
62
+
63
+ def test_routing_v2_preferences_show
64
+ assert_routing({ path: '/api/v2/preferences', method: :get }, {action: 'show', controller: 'api/v2/preferences'})
65
+ end
66
+
67
+ # V3
68
+ def test_routing_v3_posts_show
69
+ assert_routing({ path: '/api/v3/posts/1', method: :get }, {action: 'show', controller: 'api/v3/posts', id: '1'})
70
+ end
71
+
72
+ # ToDo: Refute routing
73
+ # def test_routing_v3_posts_delete
74
+ # assert_routing({ path: '/api/v3/posts/1', method: :delete }, {action: 'destroy', controller: 'api/v3/posts', id: '1'})
75
+ # end
76
+
77
+ # def test_routing_posts_links_author_except_destroy
78
+ # assert_routing({ path: '/api/v3/posts/1/links/author', method: :delete }, { controller: 'api/v3/posts', action: 'destroy_association', post_id: '1', association: 'author' })
79
+ # end
80
+ #
81
+ # def test_routing_posts_links_tags_only_create_show
82
+ # assert_routing({ path: '/api/v3/posts/1/links/tags/1,2', method: :delete }, { controller: 'api/v3/posts', action: 'destroy_association', post_id: '1', keys: '1,2', association: 'tags' })
83
+ # end
84
+ end
85
+
@@ -0,0 +1,98 @@
1
+ require 'simplecov'
2
+
3
+ # To run tests with coverage
4
+ # COVERAGE=true rake test
5
+ if ENV['COVERAGE']
6
+ SimpleCov.start do
7
+ end
8
+ end
9
+
10
+ require 'minitest/autorun'
11
+ require 'minitest/spec'
12
+ require 'rails/all'
13
+
14
+ require 'jsonapi/routing_ext'
15
+
16
+ require File.expand_path('../helpers/value_matchers', __FILE__)
17
+ require File.expand_path('../helpers/hash_helpers', __FILE__)
18
+ require File.expand_path('../helpers/functional_helpers', __FILE__)
19
+
20
+ Rails.env = 'test'
21
+
22
+ class TestApp < Rails::Application
23
+ config.eager_load = false
24
+ config.root = File.dirname(__FILE__)
25
+ config.session_store :cookie_store, key: 'session'
26
+ config.secret_key_base = 'secret'
27
+
28
+ #Raise errors on unsupported parameters
29
+ config.action_controller.action_on_unpermitted_parameters = :raise
30
+ end
31
+
32
+ TestApp.initialize!
33
+
34
+ require File.expand_path('../fixtures/active_record', __FILE__)
35
+
36
+ TestApp.routes.draw do
37
+ jsonapi_resources :authors
38
+ jsonapi_resources :people
39
+ jsonapi_resources :comments
40
+ jsonapi_resources :tags
41
+ jsonapi_resources :posts
42
+ jsonapi_resources :sections
43
+ jsonapi_resources :currencies
44
+ jsonapi_resources :expense_entries
45
+ jsonapi_resources :breeds
46
+ jsonapi_resources :planets
47
+ jsonapi_resources :planet_types
48
+ jsonapi_resources :moons
49
+ jsonapi_resources :preferences
50
+
51
+
52
+ namespace :api do
53
+ namespace :v1 do
54
+ jsonapi_resources :authors
55
+ jsonapi_resources :people
56
+ jsonapi_resources :comments
57
+ jsonapi_resources :tags
58
+ jsonapi_resources :posts
59
+ jsonapi_resources :sections
60
+ jsonapi_resources :currencies
61
+ jsonapi_resources :expense_entries
62
+ jsonapi_resources :breeds
63
+ jsonapi_resources :planets
64
+ jsonapi_resources :planet_types
65
+ jsonapi_resources :moons
66
+ jsonapi_resources :preferences
67
+ end
68
+
69
+ namespace :v2 do
70
+ jsonapi_resources :authors
71
+ jsonapi_resources :posts
72
+ jsonapi_resource :preferences
73
+ end
74
+
75
+ namespace :v3 do
76
+ jsonapi_resource :preferences do
77
+ # Intentionally empty block to skip association urls
78
+ end
79
+
80
+ jsonapi_resources :posts, except: [:destroy] do
81
+ jsonapi_link :author, except: [:destroy]
82
+ jsonapi_links :tags, only: [:show, :create]
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ class MiniTest::Unit::TestCase
89
+ include Helpers::HashHelpers
90
+ include Helpers::ValueMatchers
91
+ include Helpers::FunctionalHelpers
92
+ end
93
+
94
+ class ActiveSupport::TestCase
95
+ setup do
96
+ @routes = TestApp.routes
97
+ end
98
+ end
@@ -0,0 +1,188 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+ require File.expand_path('../../../fixtures/active_record', __FILE__)
3
+
4
+ require 'jsonapi/operation'
5
+ require 'jsonapi/operation_result'
6
+ require 'jsonapi/operations_processor'
7
+
8
+ class OperationsProcessorTest < MiniTest::Unit::TestCase
9
+ def setup
10
+ end
11
+
12
+ def test_create_single_resource
13
+ op = JSONAPI::OperationsProcessor.new()
14
+
15
+ count = Planet.count
16
+
17
+ operations = [
18
+ JSONAPI::CreateResourceOperation.new(PlanetResource, {attributes: {'name' => 'earth', 'description' => 'The best planet ever.'}})
19
+ ]
20
+
21
+ request = JSONAPI::Request.new
22
+ request.operations = operations
23
+
24
+ results = op.process(request)
25
+
26
+ assert_kind_of(Array, results)
27
+ assert_kind_of(JSONAPI::OperationResult, results[0])
28
+ assert_equal(:created, results[0].code)
29
+ assert_equal(results.size, 1)
30
+ assert_equal(Planet.count, count + 1)
31
+ end
32
+
33
+ def test_create_multiple_resources
34
+ op = JSONAPI::OperationsProcessor.new()
35
+
36
+ count = Planet.count
37
+
38
+ operations = [
39
+ JSONAPI::CreateResourceOperation.new(PlanetResource, {attributes: {'name' => 'earth', 'description' => 'The best planet for life.'}}),
40
+ JSONAPI::CreateResourceOperation.new(PlanetResource, {attributes: {'name' => 'mars', 'description' => 'The red planet.'}}),
41
+ JSONAPI::CreateResourceOperation.new(PlanetResource, {attributes: {'name' => 'venus', 'description' => 'A very hot planet.'}})
42
+ ]
43
+
44
+ request = JSONAPI::Request.new
45
+ request.operations = operations
46
+
47
+ results = op.process(request)
48
+
49
+ assert_kind_of(Array, results)
50
+ assert_equal(results.size, 3)
51
+ assert_equal(Planet.count, count + 3)
52
+ end
53
+
54
+ def test_add_has_one_association
55
+ op = JSONAPI::OperationsProcessor.new()
56
+
57
+ saturn = Planet.find(1)
58
+ gas_giant = PlanetType.find(1)
59
+ planetoid = PlanetType.find(2)
60
+ assert_equal(saturn.planet_type_id, planetoid.id)
61
+
62
+
63
+ operations = [
64
+ JSONAPI::ReplaceHasOneAssociationOperation.new(PlanetResource, saturn.id, :planet_type, gas_giant.id)
65
+ ]
66
+
67
+ request = JSONAPI::Request.new
68
+ request.operations = operations
69
+
70
+ results = op.process(request)
71
+
72
+ assert_kind_of(Array, results)
73
+ assert_kind_of(JSONAPI::OperationResult, results[0])
74
+ assert_equal(:created, results[0].code)
75
+ assert_equal(results[0].resource.object.attributes['planet_type_id'], gas_giant.id)
76
+ end
77
+
78
+ def test_add_has_many_association
79
+ op = JSONAPI::OperationsProcessor.new()
80
+
81
+ betax = Planet.find(5)
82
+ betay = Planet.find(6)
83
+ betaz = Planet.find(7)
84
+ gas_giant = PlanetType.find(1)
85
+ unknown = PlanetType.find(5)
86
+ assert_equal(betax.planet_type_id, unknown.id)
87
+ assert_equal(betay.planet_type_id, unknown.id)
88
+ assert_equal(betaz.planet_type_id, unknown.id)
89
+
90
+ operations = [
91
+ JSONAPI::CreateHasManyAssociationOperation.new(PlanetTypeResource, gas_giant.id, :planets, [betax.id, betay.id, betaz.id])
92
+ ]
93
+
94
+ request = JSONAPI::Request.new
95
+ request.operations = operations
96
+
97
+ results = op.process(request)
98
+
99
+ betax.reload
100
+ betay.reload
101
+ betaz.reload
102
+
103
+ assert_equal(betax.planet_type_id, gas_giant.id)
104
+ assert_equal(betay.planet_type_id, gas_giant.id)
105
+ assert_equal(betaz.planet_type_id, gas_giant.id)
106
+ end
107
+
108
+ def test_replace_attributes
109
+ op = JSONAPI::OperationsProcessor.new()
110
+
111
+ count = Planet.count
112
+ saturn = Planet.find(1)
113
+ assert_equal(saturn.name, 'Satern')
114
+
115
+ operations = [
116
+ JSONAPI::ReplaceFieldsOperation.new(PlanetResource, 1, {attributes: {'name' => 'saturn'}}),
117
+ ]
118
+
119
+ request = JSONAPI::Request.new
120
+ request.operations = operations
121
+
122
+ results = op.process(request)
123
+
124
+ assert_kind_of(Array, results)
125
+ assert_equal(results.size, 1)
126
+
127
+ assert_kind_of(JSONAPI::OperationResult, results[0])
128
+ assert_equal(:ok, results[0].code)
129
+
130
+ saturn = Planet.find(1)
131
+
132
+ assert_equal(saturn.name, 'saturn')
133
+
134
+ assert_equal(Planet.count, count)
135
+ end
136
+
137
+ def test_remove_resource
138
+ op = JSONAPI::OperationsProcessor.new
139
+
140
+ count = Planet.count
141
+ pluto = Planet.find(2)
142
+ assert_equal(pluto.name, 'Pluto')
143
+
144
+ operations = [
145
+ JSONAPI::RemoveResourceOperation.new(PlanetResource, 2),
146
+ ]
147
+
148
+ request = JSONAPI::Request.new
149
+ request.operations = operations
150
+
151
+ results = op.process(request)
152
+
153
+ assert_kind_of(Array, results)
154
+ assert_equal(results.size, 1)
155
+
156
+ assert_kind_of(JSONAPI::OperationResult, results[0])
157
+ assert_equal(:no_content, results[0].code)
158
+ assert_equal(Planet.count, count - 1)
159
+ end
160
+
161
+ def test_rollback_from_error
162
+ op = JSONAPI::ActiveRecordOperationsProcessor.new
163
+
164
+ count = Planet.count
165
+
166
+ operations = [
167
+ JSONAPI::RemoveResourceOperation.new(PlanetResource, 3),
168
+ JSONAPI::RemoveResourceOperation.new(PlanetResource, 4),
169
+ JSONAPI::RemoveResourceOperation.new(PlanetResource, 4)
170
+ ]
171
+
172
+ request = JSONAPI::Request.new
173
+ request.operations = operations
174
+
175
+ results = op.process(request)
176
+
177
+ assert_equal(Planet.count, count)
178
+
179
+ assert_kind_of(Array, results)
180
+ assert_equal(results.size, 3)
181
+
182
+ assert_kind_of(JSONAPI::OperationResult, results[0])
183
+ assert_equal(:no_content, results[0].code)
184
+ assert_equal(:no_content, results[1].code)
185
+ assert_equal(404, results[2].code)
186
+ end
187
+
188
+ end