jsonapi-resources 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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