user_device_spec.rb 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe UserDevice, type: :model do
  4. let(:ip) { '91.115.248.231' }
  5. before do
  6. mock_geoip_service_location(ip, {
  7. 'country_code' => 'AT',
  8. 'country_name' => 'Austria',
  9. 'city_name' => 'Graz',
  10. 'city_code' => '8010',
  11. 'continent_code' => 'EU',
  12. 'continent_name' => 'Europe',
  13. 'latitude' => 47.0833,
  14. 'longitude' => 15.5667,
  15. 'time_zone' => 'Europe/Vienna',
  16. })
  17. end
  18. describe '.add' do
  19. let(:existing_record) { described_class.add(user_agent, ip, agent.id, fingerprint, type) }
  20. let(:agent) { create(:agent) }
  21. context 'with existing record of type: "session"' do
  22. before { existing_record } # create existing record
  23. let(:user_agent) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36' }
  24. let(:fingerprint) { 'fingerprint1234' }
  25. let(:type) { 'session' }
  26. context 'when called with same parameters as existing record' do
  27. it 'returns the original record' do
  28. expect(described_class.add(user_agent, ip, agent.id, fingerprint, type))
  29. .to eq(existing_record)
  30. end
  31. end
  32. context 'when called with different IP from existing record' do
  33. let(:other_ip) { '176.198.137.254' }
  34. before do
  35. mock_geoip_service_location(other_ip, {
  36. 'country_code' => 'DE',
  37. 'country_name' => 'Germany',
  38. 'city_name' => 'Marl',
  39. 'city_code' => '45770',
  40. 'continent_code' => 'EU',
  41. 'continent_name' => 'Europe',
  42. 'latitude' => 51.6586,
  43. 'longitude' => 7.1135,
  44. 'time_zone' => 'Europe/Berlin',
  45. })
  46. end
  47. it 'returns a new record' do
  48. expect(described_class.add(user_agent, other_ip, agent.id, fingerprint, type))
  49. .to be_a(described_class)
  50. .and not_eq(existing_record)
  51. end
  52. end
  53. context 'when called with invalid IP, not matching existing record' do
  54. let(:other_ip) { 'foo' }
  55. before do
  56. mock_geoip_service_location(other_ip, {})
  57. end
  58. it 'returns a new record' do
  59. expect(described_class.add(user_agent, other_ip, agent.id, fingerprint, type))
  60. .to be_a(described_class)
  61. .and not_eq(existing_record)
  62. end
  63. end
  64. context 'when called with different fingerprint from existing record' do
  65. let(:other_fingerprint) { 'fingerprintABCD' }
  66. it 'returns a new record' do
  67. expect(described_class.add(user_agent, ip, agent.id, other_fingerprint, type))
  68. .to be_a(described_class)
  69. .and not_eq(existing_record)
  70. end
  71. end
  72. context 'with recognized user_agent (Mac/Chrome)' do
  73. it 'assigns #user_agent attribute to given value' do
  74. expect(existing_record.user_agent).to eq(user_agent)
  75. end
  76. it 'derives #name attribute from given value' do
  77. expect(existing_record.name).to eq('Mac, Chrome')
  78. end
  79. it 'derives #browser attribute from given value' do
  80. expect(existing_record.browser).to eq('Chrome')
  81. end
  82. end
  83. context 'with recognized user_agent (iOS/Safari)' do
  84. let(:user_agent) { 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H143 Safari/600.1.4' }
  85. it 'assigns #user_agent attribute to given value' do
  86. expect(existing_record.user_agent).to eq(user_agent)
  87. end
  88. it 'derives #name attribute from given value' do
  89. expect(existing_record.name).to eq('Ios, Safari')
  90. end
  91. it 'derives #browser attribute from given value' do
  92. expect(existing_record.browser).to eq('Safari')
  93. end
  94. end
  95. context 'with partially recognized user_agent (Mac/CalendarAgent)' do
  96. let(:user_agent) { 'Mac+OS+X/10.10.5 (14F27) CalendarAgent/316.1' }
  97. it 'assigns #user_agent and #browser attributes to given value' do
  98. expect([existing_record.user_agent, existing_record.browser])
  99. .to all(eq(user_agent))
  100. end
  101. it 'derives #name attribute from given value' do
  102. expect(existing_record.name).to eq("Mac, #{user_agent}")
  103. end
  104. end
  105. context 'with unrecognized user_agent' do
  106. let(:user_agent) { 'foo' }
  107. it 'assigns #user_agent, #name, and #browser attributes to given value' do
  108. expect([existing_record.user_agent, existing_record.name, existing_record.browser])
  109. .to all(eq(user_agent))
  110. end
  111. end
  112. end
  113. context 'with existing record of type: "basic_auth"' do
  114. before { existing_record } # create existing record
  115. let(:user_agent) { 'curl/7.43.0' }
  116. let(:fingerprint) { nil }
  117. let(:type) { 'basic_auth' }
  118. context 'when called with same parameters as existing record' do
  119. it 'returns the original record' do
  120. expect(described_class.add(user_agent, ip, agent.id, fingerprint, type))
  121. .to eq(existing_record)
  122. end
  123. end
  124. context 'when called with different IP from existing record' do
  125. let(:other_ip) { '176.198.137.254' }
  126. before do
  127. mock_geoip_service_location(other_ip, {
  128. 'country_code' => 'DE',
  129. 'country_name' => 'Germany',
  130. 'city_name' => 'Marl',
  131. 'city_code' => '45770',
  132. 'continent_code' => 'EU',
  133. 'continent_name' => 'Europe',
  134. 'latitude' => 51.6586,
  135. 'longitude' => 7.1135,
  136. 'time_zone' => 'Europe/Berlin',
  137. })
  138. end
  139. it 'returns a new record' do
  140. expect(described_class.add(user_agent, other_ip, agent.id, fingerprint, type))
  141. .to be_a(described_class)
  142. .and not_eq(existing_record)
  143. end
  144. end
  145. context 'when called with different type from existing record ("token_auth")' do
  146. let(:other_type) { 'token_auth' }
  147. it 'returns the original record' do
  148. expect(described_class.add(user_agent, ip, agent.id, fingerprint, other_type))
  149. .to eq(existing_record)
  150. end
  151. end
  152. context "when called without existing record's user agent" do
  153. let(:other_user_agent) { '' }
  154. it 'returns a new record' do
  155. expect(described_class.add(other_user_agent, ip, agent.id, fingerprint, type))
  156. .to be_a(described_class)
  157. .and not_eq(existing_record)
  158. end
  159. end
  160. context "when existing record's user agent is blank, and given is nil" do
  161. let(:user_agent) { '' }
  162. let(:other_user_agent) { nil }
  163. it 'returns the original record' do
  164. expect(described_class.add(other_user_agent, ip, agent.id, fingerprint, type))
  165. .to eq(existing_record)
  166. end
  167. end
  168. context "when existing record and given args have nil user agent, but IPs don't match" do
  169. let(:user_agent) { nil }
  170. let(:other_ip) { '176.198.137.254' }
  171. before do
  172. mock_geoip_service_location(other_ip, {
  173. 'country_code' => 'DE',
  174. 'country_name' => 'Germany',
  175. 'city_name' => 'Marl',
  176. 'city_code' => '45770',
  177. 'continent_code' => 'EU',
  178. 'continent_name' => 'Europe',
  179. 'latitude' => 51.6586,
  180. 'longitude' => 7.1135,
  181. 'time_zone' => 'Europe/Berlin',
  182. })
  183. end
  184. it 'returns a new record' do
  185. expect(described_class.add(user_agent, other_ip, agent.id, fingerprint, type))
  186. .to be_a(described_class)
  187. .and not_eq(existing_record)
  188. end
  189. end
  190. end
  191. context 'with exceedingly long fingerprint (161+ chars)' do
  192. let(:user_agent) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36' }
  193. let(:fingerprint) { 'x' * 161 }
  194. let(:type) { 'session' }
  195. it 'raises an error' do
  196. expect { described_class.add(user_agent, ip, agent.id, fingerprint, type) }
  197. .to raise_error(Exceptions::UnprocessableEntity)
  198. end
  199. end
  200. end
  201. describe '.action' do
  202. let(:user_device) { described_class.add(user_agent, ip, agent.id, fingerprint, type) }
  203. let(:user_agent) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36' }
  204. let(:agent) { create(:agent) }
  205. let(:fingerprint) { 'fingerprint1234' }
  206. let(:type) { 'session' }
  207. context 'when called with parameters matching given user_device' do
  208. it 'returns the given user_device' do
  209. expect(described_class.action(user_device.id, user_agent, ip, agent.id, type))
  210. .to eq(user_device)
  211. end
  212. end
  213. context 'when called with different IP from given user_device' do
  214. let(:other_ip) { '176.198.137.254' }
  215. before do
  216. mock_geoip_service_location(other_ip, {
  217. 'country_code' => 'DE',
  218. 'country_name' => 'Germany',
  219. 'city_name' => 'Marl',
  220. 'city_code' => '45770',
  221. 'continent_code' => 'EU',
  222. 'continent_name' => 'Europe',
  223. 'latitude' => 51.6586,
  224. 'longitude' => 7.1135,
  225. 'time_zone' => 'Europe/Berlin',
  226. })
  227. end
  228. it 'returns a new user_device' do
  229. expect(described_class.action(user_device.id, user_agent, other_ip, agent.id, type))
  230. .to be_a(described_class)
  231. .and not_eq(user_device)
  232. end
  233. end
  234. context 'when called with invalid IP, not matching given user_device' do
  235. let(:other_ip) { 'foo' }
  236. before do
  237. mock_geoip_service_location(other_ip, {})
  238. end
  239. it 'returns the given user_device' do
  240. expect(described_class.action(user_device.id, user_agent, other_ip, agent.id, type))
  241. .to eq(user_device)
  242. end
  243. it 'sets user_device.ip to the given (invalid) IP' do
  244. expect { described_class.action(user_device.id, user_agent, other_ip, agent.id, type) }
  245. .to change { user_device.reload.ip }.to(other_ip)
  246. end
  247. end
  248. end
  249. describe '#notification_send' do
  250. let(:user_device) { described_class.add(user_agent, ip, agent.id, fingerprint, type) }
  251. let(:user_agent) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.107 Safari/537.36' }
  252. let(:fingerprint) { 'fingerprint1234' }
  253. let(:type) { 'session' }
  254. context 'user with email address' do
  255. let(:agent) { create(:agent, email: 'somebody@example.com') }
  256. it 'returns true' do
  257. expect(user_device.notification_send('user_device_new_location'))
  258. .to be(true)
  259. end
  260. end
  261. context 'user without email address' do
  262. let(:agent) { create(:agent, email: '') }
  263. it 'returns false' do
  264. expect(user_device.notification_send('user_device_new_location'))
  265. .to be(false)
  266. end
  267. end
  268. end
  269. # Mock the location response of the GeoIP service in order to improve the stability of the test.
  270. def mock_geoip_service_location(ip, location)
  271. allow(Service::GeoIp).to receive(:location).with(ip).and_return(location)
  272. end
  273. end