history_spec.rb 8.4 KB

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