history_spec.rb 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. require 'rails_helper'
  2. require 'models/application_model_examples'
  3. require 'models/concerns/can_be_imported_examples'
  4. RSpec.describe History, type: :model do
  5. it_behaves_like 'ApplicationModel', can_assets: { own_attributes: false }
  6. it_behaves_like 'CanBeImported'
  7. describe '.list' do
  8. context 'when given an object with no histories' do
  9. let!(:object) { create(:'cti/log') }
  10. it 'returns an empty array' do
  11. expect(described_class.list(object.class.name, object.id))
  12. .to be_an(Array).and be_empty
  13. end
  14. end
  15. context 'when given an object with histories' do
  16. context 'and called without "related_history_object" argument' do
  17. let!(:object) { create(:user) }
  18. before { object.update(email: 'foo@example.com') }
  19. context 'or "assets" flag' do
  20. let(:list) { described_class.list(object.class.name, object.id) }
  21. it 'returns an array of attribute hashes for those histories' do
  22. expect(list).to match_array(
  23. [
  24. hash_including(
  25. 'o_id' => object.id,
  26. ),
  27. hash_including(
  28. 'o_id' => object.id,
  29. 'value_to' => 'foo@example.com',
  30. )
  31. ]
  32. )
  33. end
  34. it 'replaces *_id attributes with the corresponding association #name' do
  35. expect(list.first)
  36. .to not_include('history_object_id', 'history_type_id')
  37. .and include(
  38. 'object' => object.class.name,
  39. 'type' => 'created',
  40. )
  41. expect(list.second)
  42. .to not_include('history_object_id', 'history_type_id', 'history_attribute_id')
  43. .and include(
  44. 'object' => object.class.name,
  45. 'type' => 'updated',
  46. 'attribute' => 'email',
  47. )
  48. end
  49. end
  50. context 'but with "assets" flag' do
  51. let(:list) { described_class.list(object.class.name, object.id, nil, true) }
  52. let(:matching_histories) do
  53. described_class.where(
  54. o_id: object.id,
  55. history_object_id: History::Object.lookup(name: object.class.name).id
  56. )
  57. end
  58. it 'returns a hash including an array of history attribute hashes' do
  59. expect(list).to include(
  60. list: [
  61. hash_including(
  62. 'o_id' => object.id,
  63. 'object' => object.class.name,
  64. 'type' => 'created',
  65. ),
  66. hash_including(
  67. 'o_id' => object.id,
  68. 'object' => object.class.name,
  69. 'type' => 'updated',
  70. 'attribute' => 'email',
  71. 'value_to' => 'foo@example.com',
  72. )
  73. ]
  74. )
  75. end
  76. it 'returns a hash including each history record’s FE assets' do
  77. expect(list).to include(
  78. assets: matching_histories.reduce({}) { |assets, h| h.assets(assets) }
  79. )
  80. end
  81. end
  82. end
  83. context 'with "related_history_object" argument' do
  84. let!(:object) { related_object.ticket }
  85. let!(:related_object) { create(:ticket_article, internal: true) } # MUST be internal, or else callbacks will create additional histories
  86. before { object.update(title: 'Lorem ipsum dolor') }
  87. context 'but no "assets" flag' do
  88. let(:list) { described_class.list(object.class.name, object.id, 'Ticket::Article') }
  89. it 'returns an array of attribute hashes for those histories' do
  90. expect(list).to match_array(
  91. [
  92. hash_including(
  93. 'o_id' => object.id,
  94. ),
  95. hash_including(
  96. 'o_id' => related_object.id,
  97. ),
  98. hash_including(
  99. 'o_id' => object.id,
  100. 'value_to' => 'Lorem ipsum dolor',
  101. )
  102. ]
  103. )
  104. end
  105. it 'replaces *_id attributes with the corresponding association #name' do
  106. expect(list.first)
  107. .to not_include('history_object_id', 'history_type_id')
  108. .and include(
  109. 'object' => object.class.name,
  110. 'type' => 'created',
  111. )
  112. expect(list.second)
  113. .to not_include('history_object_id', 'history_type_id')
  114. .and include(
  115. 'object' => related_object.class.name,
  116. 'type' => 'created',
  117. )
  118. expect(list.third)
  119. .to not_include('history_object_id', 'history_type_id', 'history_attribute_id')
  120. .and include(
  121. 'object' => object.class.name,
  122. 'type' => 'updated',
  123. 'attribute' => 'title',
  124. )
  125. end
  126. end
  127. context 'and "assets" flag' do
  128. let(:list) { described_class.list(object.class.name, object.id, 'Ticket::Article', true) }
  129. let(:matching_histories) do
  130. described_class.where(
  131. o_id: object.id,
  132. history_object_id: History::Object.lookup(name: object.class.name).id
  133. ) + described_class.where(
  134. o_id: related_object.id,
  135. history_object_id: History::Object.lookup(name: related_object.class.name).id
  136. )
  137. end
  138. it 'returns a hash including an array of history attribute hashes' do
  139. expect(list).to include(
  140. list: [
  141. hash_including(
  142. 'o_id' => object.id,
  143. 'object' => object.class.name,
  144. 'type' => 'created',
  145. ),
  146. hash_including(
  147. 'o_id' => related_object.id,
  148. 'object' => related_object.class.name,
  149. 'type' => 'created',
  150. ),
  151. hash_including(
  152. 'o_id' => object.id,
  153. 'object' => object.class.name,
  154. 'type' => 'updated',
  155. 'attribute' => 'title',
  156. 'value_to' => 'Lorem ipsum dolor',
  157. )
  158. ]
  159. )
  160. end
  161. it 'returns a hash including each history record’s FE assets' do
  162. expect(list).to include(
  163. assets: matching_histories.reduce({}) { |assets, h| h.assets(assets) }
  164. )
  165. end
  166. end
  167. end
  168. end
  169. end
  170. shared_examples 'lookup and create if needed' do |prefix|
  171. let(:prefix) { prefix }
  172. let(:value_string) { Faker::Lorem.word }
  173. let(:value_symbol) { value_string.to_sym }
  174. let(:method_name) { "#{prefix}_lookup" }
  175. let(:cache_key) { "#{described_class}::#{prefix.capitalize}::#{value_string}" }
  176. context 'when object does not exist' do
  177. it 'creates with a given String' do
  178. expect(described_class.send(method_name, value_string)).to be_present
  179. end
  180. it 'creates with a given Symbol' do
  181. expect(described_class.send(method_name, value_symbol)).to be_present
  182. end
  183. end
  184. context 'when object exists' do
  185. before do
  186. described_class.send(method_name, value_string)
  187. end
  188. it 'retrieves object with a given String' do
  189. expect(described_class.send(method_name, value_string)).to be_present
  190. end
  191. it 'hits cache with a given String' do
  192. allow(Rails.cache).to receive(:read)
  193. described_class.send(method_name, value_string)
  194. expect(Rails.cache).to have_received(:read).with(cache_key)
  195. end
  196. it 'retrieves object with a given Symbol' do
  197. expect(described_class.send(method_name, value_symbol)).to be_present
  198. end
  199. it 'hits cache with a given Symbol' do
  200. allow(Rails.cache).to receive(:read)
  201. described_class.send(method_name, value_symbol)
  202. expect(Rails.cache).to have_received(:read).with(cache_key)
  203. end
  204. end
  205. end
  206. # https://github.com/zammad/zammad/issues/3121
  207. describe '.type_lookup' do
  208. include_examples 'lookup and create if needed', 'type'
  209. end
  210. # https://github.com/zammad/zammad/issues/3121
  211. describe '.object_lookup' do
  212. include_examples 'lookup and create if needed', 'object'
  213. end
  214. end