fortnox-api 0.8.2 → 0.9.0

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 (205) hide show
  1. checksums.yaml +4 -4
  2. data/.env.template +7 -0
  3. data/.env.test +11 -3
  4. data/.gitignore +7 -1
  5. data/.rubocop.yml +17 -1
  6. data/.travis.yml +10 -9
  7. data/CHANGELOG.md +22 -8
  8. data/CONTRIBUTE.md +21 -9
  9. data/DEVELOPER_README.md +72 -0
  10. data/Guardfile +13 -4
  11. data/README.md +226 -64
  12. data/Rakefile +128 -0
  13. data/bin/get_tokens +79 -0
  14. data/bin/renew_tokens +28 -0
  15. data/fortnox-api.gemspec +10 -9
  16. data/lib/fortnox/api/mappers/base/from_json.rb +4 -3
  17. data/lib/fortnox/api/mappers/base/to_json.rb +2 -3
  18. data/lib/fortnox/api/models/base.rb +12 -10
  19. data/lib/fortnox/api/models/customer.rb +55 -55
  20. data/lib/fortnox/api/models/label.rb +2 -2
  21. data/lib/fortnox/api/repositories/authentication.rb +61 -0
  22. data/lib/fortnox/api/repositories/base/savers.rb +3 -1
  23. data/lib/fortnox/api/repositories/base.rb +21 -35
  24. data/lib/fortnox/api/repositories.rb +1 -0
  25. data/lib/fortnox/api/request_handling.rb +30 -18
  26. data/lib/fortnox/api/types/document_row.rb +3 -3
  27. data/lib/fortnox/api/types/enums.rb +27 -11
  28. data/lib/fortnox/api/types/model.rb +1 -4
  29. data/lib/fortnox/api/types/sized.rb +2 -2
  30. data/lib/fortnox/api/types.rb +14 -1
  31. data/lib/fortnox/api/version.rb +1 -1
  32. data/lib/fortnox/api.rb +12 -32
  33. data/spec/fortnox/api/mappers/base/canonical_name_sym_spec.rb +4 -4
  34. data/spec/fortnox/api/mappers/base/from_json_spec.rb +10 -12
  35. data/spec/fortnox/api/mappers/base/to_json_spec.rb +48 -57
  36. data/spec/fortnox/api/mappers/base_spec.rb +4 -7
  37. data/spec/fortnox/api/mappers/contexts/json_conversion.rb +38 -33
  38. data/spec/fortnox/api/mappers/unit_spec.rb +3 -4
  39. data/spec/fortnox/api/models/base_spec.rb +27 -16
  40. data/spec/fortnox/api/models/unit_spec.rb +5 -3
  41. data/spec/fortnox/api/repositories/article_spec.rb +14 -9
  42. data/spec/fortnox/api/repositories/authentication_spec.rb +103 -0
  43. data/spec/fortnox/api/repositories/base_spec.rb +106 -319
  44. data/spec/fortnox/api/repositories/customer_spec.rb +37 -7
  45. data/spec/fortnox/api/repositories/examples/all.rb +0 -1
  46. data/spec/fortnox/api/repositories/examples/find.rb +5 -8
  47. data/spec/fortnox/api/repositories/examples/only.rb +4 -13
  48. data/spec/fortnox/api/repositories/examples/save.rb +32 -18
  49. data/spec/fortnox/api/repositories/examples/save_with_nested_model.rb +0 -5
  50. data/spec/fortnox/api/repositories/examples/save_with_specially_named_attribute.rb +1 -4
  51. data/spec/fortnox/api/repositories/examples/search.rb +4 -7
  52. data/spec/fortnox/api/repositories/invoice_spec.rb +64 -15
  53. data/spec/fortnox/api/repositories/order_spec.rb +11 -9
  54. data/spec/fortnox/api/repositories/project_spec.rb +7 -6
  55. data/spec/fortnox/api/repositories/terms_of_payment_spec.rb +9 -7
  56. data/spec/fortnox/api/repositories/unit_spec.rb +13 -11
  57. data/spec/fortnox/api/types/country_spec.rb +1 -1
  58. data/spec/fortnox/api/types/email_spec.rb +2 -2
  59. data/spec/fortnox/api/types/examples/document_row.rb +3 -3
  60. data/spec/fortnox/api/types/examples/enum.rb +4 -4
  61. data/spec/fortnox/api/types/examples/types.rb +1 -3
  62. data/spec/fortnox/api/types/housework_types_spec.rb +54 -61
  63. data/spec/fortnox/api/types/model_spec.rb +3 -27
  64. data/spec/fortnox/api/types/order_row_spec.rb +2 -2
  65. data/spec/fortnox/api/types/required_spec.rb +6 -11
  66. data/spec/fortnox/api/types/sales_account_spec.rb +57 -0
  67. data/spec/fortnox/api_spec.rb +19 -124
  68. data/spec/spec_helper.rb +0 -14
  69. data/spec/support/helpers/configuration_helper.rb +30 -3
  70. data/spec/support/helpers.rb +1 -1
  71. data/spec/support/matchers/type/attribute_matcher.rb +2 -2
  72. data/spec/support/matchers/type/have_nullable_date_matcher.rb +6 -4
  73. data/spec/support/matchers/type/have_nullable_matcher.rb +1 -1
  74. data/spec/support/matchers/type/have_nullable_string_matcher.rb +5 -5
  75. data/spec/support/matchers/type/require_attribute_matcher.rb +5 -5
  76. data/spec/support/matchers/type/type_matcher.rb +1 -1
  77. data/spec/support/vcr_setup.rb +16 -0
  78. data/spec/vcr_cassettes/articles/all.yml +16 -43
  79. data/spec/vcr_cassettes/articles/find_by_hash_failure.yml +10 -12
  80. data/spec/vcr_cassettes/articles/find_failure.yml +10 -12
  81. data/spec/vcr_cassettes/articles/find_id_1.yml +13 -14
  82. data/spec/vcr_cassettes/articles/find_new.yml +14 -16
  83. data/spec/vcr_cassettes/articles/multi_param_find_by_hash.yml +13 -15
  84. data/spec/vcr_cassettes/articles/save_new.yml +13 -15
  85. data/spec/vcr_cassettes/articles/save_old.yml +14 -16
  86. data/spec/vcr_cassettes/articles/save_with_specially_named_attribute.yml +13 -15
  87. data/spec/vcr_cassettes/articles/search_by_name.yml +16 -15
  88. data/spec/vcr_cassettes/articles/search_miss.yml +10 -12
  89. data/spec/vcr_cassettes/articles/search_with_special_char.yml +10 -12
  90. data/spec/vcr_cassettes/articles/single_param_find_by_hash.yml +13 -27
  91. data/spec/vcr_cassettes/authentication/expired_token.yml +54 -0
  92. data/spec/vcr_cassettes/authentication/invalid_authorization.yml +57 -0
  93. data/spec/vcr_cassettes/authentication/invalid_refresh_token.yml +58 -0
  94. data/spec/vcr_cassettes/authentication/valid_request.yml +63 -0
  95. data/spec/vcr_cassettes/customers/all.yml +20 -127
  96. data/spec/vcr_cassettes/customers/find_by_hash_failure.yml +10 -12
  97. data/spec/vcr_cassettes/customers/find_failure.yml +10 -12
  98. data/spec/vcr_cassettes/customers/find_id_1.yml +14 -15
  99. data/spec/vcr_cassettes/customers/find_new.yml +13 -15
  100. data/spec/vcr_cassettes/customers/find_with_sales_account.yml +63 -0
  101. data/spec/vcr_cassettes/customers/multi_param_find_by_hash.yml +13 -15
  102. data/spec/vcr_cassettes/customers/save_new.yml +12 -14
  103. data/spec/vcr_cassettes/customers/save_new_with_country_code_SE.yml +12 -14
  104. data/spec/vcr_cassettes/customers/save_new_with_sales_account.yml +63 -0
  105. data/spec/vcr_cassettes/customers/save_old.yml +13 -15
  106. data/spec/vcr_cassettes/customers/save_with_specially_named_attribute.yml +12 -14
  107. data/spec/vcr_cassettes/customers/search_by_name.yml +13 -45
  108. data/spec/vcr_cassettes/customers/search_miss.yml +10 -12
  109. data/spec/vcr_cassettes/customers/search_with_special_char.yml +10 -12
  110. data/spec/vcr_cassettes/customers/single_param_find_by_hash.yml +14 -16
  111. data/spec/vcr_cassettes/invoices/all.yml +47 -112
  112. data/spec/vcr_cassettes/invoices/filter_hit.yml +14 -18
  113. data/spec/vcr_cassettes/invoices/filter_invalid.yml +10 -12
  114. data/spec/vcr_cassettes/invoices/find_by_hash_failure.yml +10 -12
  115. data/spec/vcr_cassettes/invoices/find_failure.yml +10 -12
  116. data/spec/vcr_cassettes/invoices/find_id_1.yml +15 -16
  117. data/spec/vcr_cassettes/invoices/find_new.yml +16 -18
  118. data/spec/vcr_cassettes/invoices/multi_param_find_by_hash.yml +13 -15
  119. data/spec/vcr_cassettes/invoices/row_description_limit.yml +65 -0
  120. data/spec/vcr_cassettes/invoices/save_new.yml +14 -16
  121. data/spec/vcr_cassettes/invoices/save_new_with_comments.yml +14 -16
  122. data/spec/vcr_cassettes/invoices/save_new_with_country.yml +14 -15
  123. data/spec/vcr_cassettes/invoices/save_new_with_country_GB.yml +15 -16
  124. data/spec/vcr_cassettes/invoices/save_new_with_country_Norge.yml +14 -15
  125. data/spec/vcr_cassettes/invoices/save_new_with_country_Norway.yml +14 -15
  126. data/spec/vcr_cassettes/invoices/save_new_with_country_Sverige.yml +14 -15
  127. data/spec/vcr_cassettes/invoices/save_new_with_country_VA.yml +15 -16
  128. data/spec/vcr_cassettes/invoices/save_new_with_country_VI.yml +15 -16
  129. data/spec/vcr_cassettes/invoices/save_new_with_country_empty_string.yml +14 -15
  130. data/spec/vcr_cassettes/invoices/save_new_with_country_nil.yml +14 -15
  131. data/spec/vcr_cassettes/invoices/save_new_with_unsaved_parent.yml +65 -0
  132. data/spec/vcr_cassettes/invoices/save_old.yml +16 -18
  133. data/spec/vcr_cassettes/invoices/save_old_with_empty_comments.yml +16 -18
  134. data/spec/vcr_cassettes/invoices/save_old_with_empty_country.yml +16 -17
  135. data/spec/vcr_cassettes/invoices/save_old_with_nil_comments.yml +16 -18
  136. data/spec/vcr_cassettes/invoices/save_old_with_nil_country.yml +16 -17
  137. data/spec/vcr_cassettes/invoices/save_with_nested_model.yml +15 -16
  138. data/spec/vcr_cassettes/invoices/save_with_specially_named_attribute.yml +14 -15
  139. data/spec/vcr_cassettes/invoices/search_by_name.yml +13 -21
  140. data/spec/vcr_cassettes/invoices/search_miss.yml +10 -12
  141. data/spec/vcr_cassettes/invoices/search_with_special_char.yml +10 -12
  142. data/spec/vcr_cassettes/invoices/single_param_find_by_hash.yml +14 -16
  143. data/spec/vcr_cassettes/orders/all.yml +19 -113
  144. data/spec/vcr_cassettes/orders/filter_hit.yml +14 -20
  145. data/spec/vcr_cassettes/orders/filter_invalid.yml +10 -12
  146. data/spec/vcr_cassettes/orders/find_by_hash_failure.yml +10 -12
  147. data/spec/vcr_cassettes/orders/find_failure.yml +10 -12
  148. data/spec/vcr_cassettes/orders/find_id_1.yml +17 -17
  149. data/spec/vcr_cassettes/orders/find_new.yml +16 -18
  150. data/spec/vcr_cassettes/orders/housework_invalid_tax_reduction_type.yml +11 -13
  151. data/spec/vcr_cassettes/orders/housework_othercoses_invalid.yml +11 -13
  152. data/spec/vcr_cassettes/orders/housework_type_babysitting.yml +15 -16
  153. data/spec/vcr_cassettes/orders/housework_type_cleaning.yml +15 -16
  154. data/spec/vcr_cassettes/orders/housework_type_construction.yml +15 -16
  155. data/spec/vcr_cassettes/orders/housework_type_cooking.yml +11 -13
  156. data/spec/vcr_cassettes/orders/housework_type_electricity.yml +15 -16
  157. data/spec/vcr_cassettes/orders/housework_type_gardening.yml +15 -16
  158. data/spec/vcr_cassettes/orders/housework_type_glassmetalwork.yml +15 -16
  159. data/spec/vcr_cassettes/orders/housework_type_grounddrainagework.yml +15 -16
  160. data/spec/vcr_cassettes/orders/housework_type_hvac.yml +15 -16
  161. data/spec/vcr_cassettes/orders/housework_type_itservices.yml +15 -16
  162. data/spec/vcr_cassettes/orders/housework_type_majorappliancerepair.yml +15 -16
  163. data/spec/vcr_cassettes/orders/housework_type_masonry.yml +15 -16
  164. data/spec/vcr_cassettes/orders/housework_type_movingservices.yml +15 -16
  165. data/spec/vcr_cassettes/orders/housework_type_othercare.yml +15 -16
  166. data/spec/vcr_cassettes/orders/housework_type_othercosts.yml +15 -16
  167. data/spec/vcr_cassettes/orders/housework_type_paintingwallpapering.yml +15 -16
  168. data/spec/vcr_cassettes/orders/housework_type_snowplowing.yml +15 -16
  169. data/spec/vcr_cassettes/orders/housework_type_textileclothing.yml +15 -16
  170. data/spec/vcr_cassettes/orders/housework_type_tutoring.yml +11 -13
  171. data/spec/vcr_cassettes/orders/multi_param_find_by_hash.yml +13 -15
  172. data/spec/vcr_cassettes/orders/save_new.yml +16 -18
  173. data/spec/vcr_cassettes/orders/save_old.yml +16 -18
  174. data/spec/vcr_cassettes/orders/save_with_nested_model.yml +15 -16
  175. data/spec/vcr_cassettes/orders/search_by_name.yml +13 -17
  176. data/spec/vcr_cassettes/orders/search_miss.yml +10 -12
  177. data/spec/vcr_cassettes/orders/search_with_special_char.yml +10 -12
  178. data/spec/vcr_cassettes/orders/single_param_find_by_hash.yml +14 -16
  179. data/spec/vcr_cassettes/projects/all.yml +14 -37
  180. data/spec/vcr_cassettes/projects/find_by_hash_failure.yml +10 -12
  181. data/spec/vcr_cassettes/projects/find_failure.yml +10 -12
  182. data/spec/vcr_cassettes/projects/find_id_1.yml +13 -15
  183. data/spec/vcr_cassettes/projects/find_new.yml +14 -16
  184. data/spec/vcr_cassettes/projects/multi_param_find_by_hash.yml +15 -16
  185. data/spec/vcr_cassettes/projects/save_new.yml +13 -15
  186. data/spec/vcr_cassettes/projects/save_old.yml +14 -16
  187. data/spec/vcr_cassettes/projects/single_param_find_by_hash.yml +12 -14
  188. data/spec/vcr_cassettes/termsofpayments/all.yml +16 -23
  189. data/spec/vcr_cassettes/termsofpayments/find_failure.yml +10 -12
  190. data/spec/vcr_cassettes/termsofpayments/find_id_1.yml +13 -16
  191. data/spec/vcr_cassettes/termsofpayments/find_new.yml +12 -14
  192. data/spec/vcr_cassettes/termsofpayments/save_new.yml +12 -14
  193. data/spec/vcr_cassettes/termsofpayments/save_old.yml +12 -14
  194. data/spec/vcr_cassettes/units/all.yml +13 -24
  195. data/spec/vcr_cassettes/units/find_failure.yml +10 -12
  196. data/spec/vcr_cassettes/units/find_id_1.yml +13 -15
  197. data/spec/vcr_cassettes/units/find_new.yml +12 -14
  198. data/spec/vcr_cassettes/units/save_new.yml +12 -14
  199. data/spec/vcr_cassettes/units/save_old.yml +12 -14
  200. data/spec/vcr_cassettes/units/save_with_specially_named_attribute.yml +12 -14
  201. metadata +39 -230
  202. data/lib/fortnox/api/circular_queue.rb +0 -39
  203. data/spec/fortnox/api/circular_queue_spec.rb +0 -52
  204. data/spec/support/helpers/when_performing_helper.rb +0 -7
  205. data/temp.txt +0 -1
@@ -2,380 +2,167 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'fortnox/api'
5
+ require 'jwt'
6
+ require 'dry/container/stub'
7
+
8
+ describe Fortnox::API::Repository::Base, integration: true do
9
+ include Helpers::Configuration
5
10
 
6
- describe Fortnox::API::Repository::Base do
7
11
  before do
8
- stub_const('Model::RepositoryBaseTest', Class.new)
9
- stub_const('Repository::Test', Class.new(described_class))
10
- stub_const('Repository::Test::MODEL', Model::RepositoryBaseTest)
12
+ stub_const('TestModel', Class.new)
13
+ stub_const('TestRepository', Class.new(described_class))
14
+ stub_const('TestRepository::MODEL', TestModel)
11
15
 
12
- Fortnox::API::Registry.register(:repositorybasetest, Model::RepositoryBaseTest)
13
- end
16
+ Fortnox::API::Registry.enable_stubs!
14
17
 
15
- after do
16
- # HACK: Currently, there is no way to remove keys from the Dry::Container#register.
17
- # We could move the register call to a before(:all) hook, but that registered key
18
- # would then leak into other tests. Instead, we can simply delete it with this little hack :)
19
- Fortnox::API::Registry._container.delete('repositorybasetest')
18
+ add_to_registry(:testmodel, TestModel)
20
19
  end
21
20
 
22
- let(:access_token) { '3f08d038-f380-4893-94a0-a08f6e60e67a' }
23
- let(:access_token2) { '89feajou-sif8-8f8u-29ja-xdfniokeniod' }
24
- let(:client_secret) { 'P5K5vE3Kun' }
25
- let(:repository) { Repository::Test.new }
26
- let(:application_json) {}
27
- let(:headers) do
28
- {
29
- 'Access-Token' => access_token,
30
- 'Client-Secret' => client_secret,
31
- 'Content-Type' => 'application/json',
32
- 'Accept' => 'application/json'
33
- }
34
- end
35
-
36
- describe 'creation' do
37
- shared_examples_for 'missing configuration' do
38
- subject { -> { Repository::Test.new(token_store: :store1).get('nonsense') } }
39
-
40
- let(:error) { Fortnox::API::MissingConfiguration }
41
-
42
- it { is_expected.to raise_error(error, /#{message}/) }
43
- end
44
-
45
- context 'without base url' do
46
- include_examples 'missing configuration' do
47
- before do
48
- Fortnox::API.configure do |config|
49
- config.base_url = nil
50
- config.client_secret = client_secret
51
- config.access_tokens = { store1: access_token }
52
- end
53
- end
54
-
55
- let(:message) { 'have to provide a base url' }
56
- end
57
- end
21
+ let(:repository) { TestRepository.new }
58
22
 
59
- context 'without client secret' do
60
- include_examples 'missing configuration' do
61
- before do
62
- Fortnox::API.configure do |config|
63
- config.client_secret = nil
64
- config.access_tokens = { store1: access_token }
65
- end
66
- end
23
+ describe '#initialize' do
24
+ context 'without providing an access token' do
25
+ before { Fortnox::API.access_token = nil }
67
26
 
68
- let(:message) { 'have to provide your client secret' }
27
+ it 'raises an error' do
28
+ expect { TestRepository.new }.to raise_error(Fortnox::API::MissingAccessToken)
69
29
  end
70
30
  end
71
- end
72
-
73
- describe '#next_access_token' do
74
- before { Fortnox::API.configure { |conf| conf.client_secret = client_secret } }
75
-
76
- context 'with default token store' do
77
- context 'with one access token' do
78
- subject { repository.next_access_token }
79
-
80
- before { Fortnox::API.configure { |conf| conf.access_token = access_token } }
81
-
82
- it { is_expected.to eql(access_token) }
83
- end
84
-
85
- context 'with one access token and two subsequent requests' do
86
- subject { repository.next_access_token }
87
-
88
- before do
89
- Fortnox::API.configure { |conf| conf.access_token = access_token }
90
- repository.next_access_token
91
- end
92
-
93
- it 'still uses the same token' do
94
- is_expected.to eql(access_token)
95
- end
96
- end
97
-
98
- context 'with multiple access tokens' do
99
- subject { access_tokens }
100
-
101
- before { Fortnox::API.configure { |conf| conf.access_tokens = access_tokens } }
102
- let(:access_tokens) { [access_token, access_token2] }
103
-
104
- it { is_expected.to include(repository.next_access_token) }
105
-
106
- it 'changes token on next request' do
107
- token1 = repository.next_access_token
108
- token2 = repository.next_access_token
109
-
110
- expect(token1).not_to eql(token2)
111
- end
112
-
113
- it 'circulates tokens' do
114
- token1 = repository.next_access_token
115
- repository.next_access_token
116
- token3 = repository.next_access_token
117
-
118
- expect(token1).to eql(token3)
119
- end
120
- end
121
- end
122
-
123
- context 'with multiple stores' do
124
- subject { repository.next_access_token }
125
-
126
- before do
127
- Fortnox::API.configure do |config|
128
- config.access_tokens = { store1: access_token, store2: access_token2 }
129
- end
130
- end
131
31
 
132
- describe 'first token store' do
133
- let(:repository) { Repository::Test.new(token_store: :store1) }
32
+ context 'with expired access token' do
33
+ before { Fortnox::API.access_token = ENV.fetch('EXPIRED_ACCESS_TOKEN') }
134
34
 
135
- it { is_expected.to eql access_token }
136
- end
137
-
138
- describe 'second token store' do
139
- let(:repository) { Repository::Test.new(token_store: :store2) }
140
-
141
- it { is_expected.to eql access_token2 }
35
+ it 'raises an error' do
36
+ expect do
37
+ VCR.use_cassette('authentication/expired_token') { repository.get('/customers', body: '') }
38
+ end.to raise_error(Fortnox::API::RemoteServerError, /Unauthorized request(.)*"message":"unauthorized"/)
142
39
  end
143
40
  end
144
41
  end
145
42
 
146
- describe '#access_tokens' do
147
- subject(:access_tokens) { repository.access_tokens }
148
-
149
- before { Fortnox::API.configure { |conf| conf.client_secret = client_secret } }
150
-
151
- let(:token_store_not_present) do
152
- "There is no token store named #{token_store.inspect}. Available stores are #{available_stores}."
153
- end
154
-
155
- let(:error) { Fortnox::API::MissingConfiguration }
156
-
157
- context 'with non existing token store' do
158
- subject { -> { access_tokens } }
159
-
160
- before do
161
- Fortnox::API.configure { |conf| conf.access_tokens = { some_store: [access_token] } }
162
- end
163
-
164
- let(:repository) { Repository::Test.new(token_store: token_store) }
165
- let(:token_store) { :non_existing_store }
166
- let(:available_stores) { [:some_store] }
167
-
168
- it { is_expected.to raise_error(error, token_store_not_present) }
169
- end
170
-
171
- context 'with no tokens set' do
172
- subject { -> { access_tokens } }
173
-
174
- before { Fortnox::API.configure { |conf| conf.access_tokens = {} } }
175
- let(:token_store) { :default }
176
- let(:available_stores) { [] }
177
-
178
- it { is_expected.to raise_error(error, token_store_not_present) }
179
- end
180
-
181
- context 'with one access token in token store' do
182
- before { Fortnox::API.configure { |conf| conf.access_token = access_token } }
183
- let(:token_store) { :default }
184
-
185
- it { is_expected.to eql(access_token) }
186
- end
187
-
188
- context 'with multiple access tokens' do
189
- before do
190
- Fortnox::API.configure { |conf| conf.access_tokens = [access_token, access_token2] }
191
- end
192
- let(:token_store) { :default }
193
-
194
- it { is_expected.to eql([access_token, access_token2]) }
195
- end
196
-
197
- context 'with multiple token stores' do
198
- before do
199
- Fortnox::API.configure do |conf|
200
- conf.access_tokens = { store_a: store_a_tokens, store_b: store_b_token }
201
- end
202
- end
203
- let(:store_a_tokens) { %w[token_a1 token_a2] }
204
- let(:store_b_token) { 'token_b1' }
205
-
206
- context 'with valid store name' do
207
- let(:repository) { Repository::Test.new(token_store: :store_a) }
208
-
209
- it { is_expected.to eql(store_a_tokens) }
210
- end
211
-
212
- context 'with non collection' do
213
- let(:repository) { Repository::Test.new(token_store: :store_b) }
214
-
215
- it { is_expected.to eql(store_b_token) }
216
- end
217
-
218
- context 'with invalid store name' do
219
- subject { -> { access_tokens } }
220
-
221
- let(:repository) { Repository::Test.new(token_store: :nonsence_store) }
222
-
223
- it { is_expected.to raise_error(error) }
224
- end
225
- end
226
- end
227
-
228
- describe '#check_access_tokens!' do
229
- subject { -> { repository.check_access_tokens!(tokens) } }
230
-
231
- before { Fortnox::API.configure { |conf| conf.client_secret = client_secret } }
232
- let(:error) { Fortnox::API::MissingConfiguration }
233
- let(:message) { "not provided any access tokens in token store #{token_store.inspect}" }
234
- let(:token_store) { :default }
235
-
236
- context 'with nil' do
237
- let(:tokens) { nil }
238
-
239
- it { is_expected.to raise_error(error, /#{message}/) }
240
- end
241
-
242
- context 'with empty array' do
243
- let(:tokens) { [] }
244
-
245
- it { is_expected.to raise_error(error, /#{message}/) }
246
- end
247
-
248
- context 'with an empty, non default, token store' do
249
- before { Fortnox::API.configure { |conf| conf.access_tokens = { token_store => tokens } } }
250
- let(:repository) { Repository::Test.new(token_store: token_store) }
251
- let(:tokens) { [] }
252
- let(:token_store) { :store1 }
253
-
254
- it { is_expected.to raise_error(error, /#{message}/) }
255
- end
256
-
257
- context 'with valid tokens' do
258
- let(:tokens) { %w[12345 abcde] }
259
-
260
- it { is_expected.not_to raise_error }
261
- end
262
- end
263
-
264
43
  context 'when making a request including the proper headers' do
44
+ subject { repository.get('/test', body: '') }
45
+
265
46
  before do
266
- Fortnox::API.configure do |conf|
267
- conf.client_secret = client_secret
268
- conf.access_token = access_token
269
- end
47
+ set_api_test_configuration
270
48
 
271
49
  stub_request(
272
50
  :get,
273
51
  'https://api.fortnox.se/3/test'
274
- ).with(
275
- headers: headers
276
52
  ).to_return(
277
53
  status: 200
278
54
  )
279
55
  end
280
56
 
281
- subject { repository.get('/test', body: '') }
282
-
283
57
  it { is_expected.to be_nil }
284
58
  end
285
59
 
286
- describe 'making requests with multiple access tokens' do
60
+ context 'when receiving an error from remote server' do
287
61
  before do
288
- Fortnox::API.configure do |conf|
289
- conf.client_secret = client_secret
290
- conf.access_tokens = [access_token, access_token2]
291
- end
62
+ set_api_test_configuration
292
63
 
293
64
  stub_request(
294
- :get,
65
+ :post,
295
66
  'https://api.fortnox.se/3/test'
296
- ).with(
297
- headers: {
298
- 'Access-Token' => access_token,
299
- 'Client-Secret' => client_secret,
300
- 'Content-Type' => 'application/json',
301
- 'Accept' => 'application/json'
302
- }
303
67
  ).to_return(
304
- status: 200,
305
- body: '1'
68
+ status: 500,
69
+ body: {
70
+ 'ErrorInformation' => {
71
+ 'error' => 1,
72
+ 'message' => 'some-error-message'
73
+ }
74
+ }.to_json,
75
+ headers: { 'Content-Type' => 'application/json' }
306
76
  )
77
+ end
307
78
 
308
- stub_request(
309
- :get,
310
- 'https://api.fortnox.se/3/test'
311
- ).with(
312
- headers: {
313
- 'Access-Token' => access_token2,
314
- 'Client-Secret' => client_secret,
315
- 'Content-Type' => 'application/json',
316
- 'Accept' => 'application/json'
317
- }
318
- ).to_return(
319
- status: 200,
320
- body: '2'
321
- )
79
+ it 'raises an error' do
80
+ expect do
81
+ repository.post('/test', body: '')
82
+ end.to raise_error(Fortnox::API::RemoteServerError, /some-error-message/)
322
83
  end
323
84
 
324
- context 'with subsequent requests on same object' do
325
- let!(:response1) { repository.get('/test', body: '') }
326
- let!(:response2) { repository.get('/test', body: '') }
327
- let!(:response3) { repository.get('/test', body: '') }
85
+ context 'with debugging enabled' do
86
+ before { Fortnox::API.config.debugging = true }
87
+ after { Fortnox::API.config.debugging = false }
328
88
 
329
- # rubocop:disable RSpec/MultipleExpectations
330
- # All these checks must be run in same it-statement because
331
- # of the random starting index.
332
- it 'works' do
333
- expect(response1).not_to eq(response2)
334
- expect(response1).to eq(response3)
335
- expect(response3).not_to eq(response2)
89
+ it do
90
+ expect do
91
+ repository.post('/test', body: '')
92
+ end.to raise_error(/<HTTParty::Request:0x/)
336
93
  end
337
- # rubocop:enable RSpec/MultipleExpectations
338
94
  end
339
95
  end
340
96
 
341
- context 'when raising error from remote server' do
342
- error_message = 'Räkenskapsår finns inte upplagt. '\
343
- 'För att kunna skapa en faktura krävs det att det finns ett räkenskapsår'
344
-
97
+ context 'when receiving HTML from remote server' do
345
98
  before do
346
- Fortnox::API.configure do |conf|
347
- conf.client_secret = client_secret
348
- conf.access_tokens = [access_token, access_token2]
349
- end
99
+ set_api_test_configuration
350
100
 
351
101
  stub_request(
352
- :post,
353
- 'https://api.fortnox.se/3/test'
102
+ :get,
103
+ 'https://api.fortnox.se/3nonsense'
354
104
  ).to_return(
355
- status: 500,
356
- body: {
357
- 'ErrorInformation' => {
358
- 'error' => 1,
359
- 'message' => error_message
360
- }
361
- }.to_json,
362
- headers: { 'Content-Type' => 'application/json' }
105
+ status: 404,
106
+ body: "<html>\r\n" \
107
+ "<head><title>404 Not Found</title></head>\r\n" \
108
+ "<body>\r\n<center><h1>404 Not Found</h1></center>\r\n" \
109
+ "<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n",
110
+ headers: { 'Content-Type' => 'text/html' }
363
111
  )
364
112
  end
365
113
 
366
- subject { -> { repository.post('/test', body: '') } }
114
+ it 'raises an error' do
115
+ expect do
116
+ repository.get('nonsense')
117
+ end.to raise_error(Fortnox::API::RemoteServerError,
118
+ %r{Fortnox API's response has content type "text/html"})
119
+ end
120
+ end
367
121
 
368
- it { is_expected.to raise_error(Fortnox::API::RemoteServerError) }
369
- it { is_expected.to raise_error(error_message) }
122
+ describe 'update existing' do
123
+ context 'with no change' do
124
+ before do
125
+ set_api_test_configuration
370
126
 
371
- context 'with debugging enabled' do
372
- around do |example|
373
- Fortnox::API.config.debugging = true
374
- example.run
375
- Fortnox::API.config.debugging = false
127
+ stub_const(
128
+ 'Product',
129
+ Class.new(Fortnox::API::Model::Base) do
130
+ attribute :name, 'strict.string'
131
+ attribute :description, 'string' # nullable
132
+ end
133
+ )
134
+ stub_const('Product::STUB', { name: '' }.freeze)
135
+ stub_const('Product::UNIQUE_ID', :name)
136
+
137
+ stub_const('ProductMapper', Class.new(Fortnox::API::Mapper::Base))
138
+ stub_const('ProductMapper::JSON_ENTITY_WRAPPER', 'Product')
139
+ stub_const('ProductMapper::JSON_COLLECTION_WRAPPER', 'Products')
140
+ stub_const('ProductMapper::KEY_MAP', {})
141
+
142
+ stub_const('TestRepository::MODEL', Product)
143
+ stub_const('TestRepository::MAPPER', ProductMapper)
144
+ stub_const('TestRepository::URI', '/test/')
145
+
146
+ Fortnox::API::Registry.enable_stubs!
147
+ add_to_registry(:product, ProductMapper)
148
+
149
+ stub_request(
150
+ :post,
151
+ 'https://api.fortnox.se/3/test/'
152
+ ).to_return(
153
+ status: 200,
154
+ body: '{"Product" : {"Name": "test", "Desription": null}}',
155
+ headers: { 'Content-Type' => 'application/json' }
156
+ ).times(1) # Only stub the first save request
157
+ end
158
+
159
+ let(:updated_entity) do
160
+ repository.save(Product.new(name: 'test')).update({ description: nil })
376
161
  end
377
162
 
378
- it { is_expected.to raise_error(/\<HTTParty\:\:Request\:0x/) }
163
+ it 'does not call Fortnox' do
164
+ expect { repository.save(updated_entity) }.not_to raise_error
165
+ end
379
166
  end
380
167
  end
381
168
  end
@@ -10,13 +10,14 @@ require 'fortnox/api/repositories/examples/save'
10
10
  require 'fortnox/api/repositories/examples/save_with_specially_named_attribute'
11
11
  require 'fortnox/api/repositories/examples/search'
12
12
 
13
- describe Fortnox::API::Repository::Customer, order: :defined, integration: true do
13
+ describe Fortnox::API::Repository::Customer, integration: true, order: :defined do
14
14
  include Helpers::Configuration
15
-
16
- before { set_api_test_configuration }
15
+ include Helpers::Repositories
17
16
 
18
17
  subject(:repository) { described_class.new }
19
18
 
19
+ before { set_api_test_configuration }
20
+
20
21
  include_examples '.save', :name
21
22
 
22
23
  include_examples '.save with specially named attribute',
@@ -24,11 +25,12 @@ describe Fortnox::API::Repository::Customer, order: :defined, integration: true
24
25
  :email_invoice_cc,
25
26
  'test@example.com'
26
27
 
27
- # It is not yet possible to delete Customers. Therefore, expected nr of
28
+ # VCR: It is not yet possible to delete Customers. Therefore, expected nr of
28
29
  # Customers when running .all will continue to increase
29
30
  # (until 100, which is max by default).
30
- include_examples '.all', 100
31
+ include_examples '.all', 7
31
32
 
33
+ # VCR: Searched Customers needs to be created manually
32
34
  include_examples '.find', '1' do
33
35
  let(:find_by_hash_failure) { { city: 'Not Found' } }
34
36
  let(:single_param_find_by_hash) { { find_hash: { city: 'New York' }, matches: 2 } }
@@ -38,8 +40,8 @@ describe Fortnox::API::Repository::Customer, order: :defined, integration: true
38
40
  end
39
41
  end
40
42
 
41
- # When recording new VCR casettes, expected matches must be increased
42
- include_examples '.search', :name, 'Test', 33
43
+ # NOTE: When recording new VCR casettes, expected matches must be increased
44
+ include_examples '.search', :name, 'Test', 2
43
45
 
44
46
  describe 'country reference' do
45
47
  describe 'with valid country code \'SE\'' do
@@ -67,4 +69,32 @@ describe Fortnox::API::Repository::Customer, order: :defined, integration: true
67
69
  end
68
70
  end
69
71
  end
72
+
73
+ describe 'sales account' do
74
+ # VCR: The Sales Account needs to be created manually in Fortnox
75
+ context 'when saving a Customer with a Sales Account set' do
76
+ let(:customer) do
77
+ VCR.use_cassette("#{vcr_dir}/save_new_with_sales_account") do
78
+ repository.save(
79
+ described_class::MODEL.new(
80
+ name: 'Customer with Sales Account',
81
+ sales_account: '3001'
82
+ )
83
+ )
84
+ end
85
+ end
86
+
87
+ context 'when fetching that Customer' do
88
+ subject { fetched_customer.sales_account }
89
+
90
+ let(:fetched_customer) do
91
+ VCR.use_cassette("#{vcr_dir}/find_with_sales_account") do
92
+ repository.find(customer.customer_number)
93
+ end
94
+ end
95
+
96
+ it { is_expected.to eq('3001') }
97
+ end
98
+ end
99
+ end
70
100
  end
@@ -15,4 +15,3 @@ RSpec.shared_examples_for '.all' do |count|
15
15
  end
16
16
  end
17
17
  end
18
- # rubocop:enable RSpec/DescribeClass
@@ -28,16 +28,13 @@ shared_examples_for '.find' do |searched_entity_id, find_by_hash: true|
28
28
  end
29
29
 
30
30
  context 'when not found' do
31
- subject { find_failure }
32
-
33
- let(:not_found_id) { '123456789' }
34
- let(:find_failure) do
35
- when_performing do
36
- VCR.use_cassette("#{vcr_dir}/find_failure") { repository.find(not_found_id) }
37
- end
31
+ let(:find_with_non_existing_id) do
32
+ VCR.use_cassette("#{vcr_dir}/find_failure") { repository.find('123456789') }
38
33
  end
39
34
 
40
- it { is_expected.to raise_error(Fortnox::API::RemoteServerError, /Kan inte hitta /) }
35
+ specify do
36
+ expect { find_with_non_existing_id }.to raise_error(Fortnox::API::RemoteServerError, /Kan inte hitta /)
37
+ end
41
38
  end
42
39
  end
43
40
 
@@ -2,14 +2,8 @@
2
2
 
3
3
  shared_examples_for '.only' do |matching_filter, expected_matches, missing_filter: nil|
4
4
  describe '.only' do
5
- def repository_only(repository, vcr_cassette, filter)
6
- VCR.use_cassette("#{vcr_dir}/#{vcr_cassette}") do
7
- repository.only(filter)
8
- end
9
- end
10
-
11
5
  shared_examples '.only response' do |vcr_cassette, expected_entries|
12
- subject { repository_only(repository, vcr_cassette, filter) }
6
+ subject { VCR.use_cassette("#{vcr_dir}/#{vcr_cassette}") { repository.only(filter) } }
13
7
 
14
8
  it { is_expected.to be_instance_of(Array) }
15
9
  it { is_expected.to have(expected_entries).entries }
@@ -30,14 +24,11 @@ shared_examples_for '.only' do |matching_filter, expected_matches, missing_filte
30
24
  end
31
25
 
32
26
  context 'with invalid filter' do
33
- subject do
34
- when_performing do
35
- repository_only(repository, 'filter_invalid', 'doesntexist')
36
- end
27
+ let(:only_with_invalid_filter) do
28
+ VCR.use_cassette("#{vcr_dir}/filter_invalid") { repository.only('doesntexist') }
37
29
  end
38
30
 
39
- it { is_expected.to raise_error(Fortnox::API::RemoteServerError, /ogiltigt filter/) }
31
+ specify { expect { only_with_invalid_filter }.to raise_error(Fortnox::API::RemoteServerError, /ogiltigt filter/) }
40
32
  end
41
33
  end
42
34
  end
43
- # rubocop:enable RSpec/DescribeClass