payload_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Whatsapp::Webhook::Payload, :aggregate_failures, current_user_id: 1 do
  4. let(:channel) { create(:whatsapp_channel) }
  5. let(:from) do
  6. {
  7. phone: Faker::PhoneNumber.cell_phone_in_e164.delete('+'),
  8. name: Faker::Name.unique.name
  9. }
  10. end
  11. let(:user_data) do
  12. firstname, lastname = User.name_guess(from[:name])
  13. # Fallback to profile name if no firstname or lastname is found
  14. if firstname.blank? || lastname.blank?
  15. firstname, lastname = from[:name].split(%r{\s|\.|,|,\s}, 2)
  16. end
  17. {
  18. firstname: firstname&.strip,
  19. lastname: lastname&.strip,
  20. mobile: "+#{from[:phone]}",
  21. login: from[:phone],
  22. }
  23. end
  24. let(:event) { 'messages' }
  25. let(:type) { 'text' }
  26. let(:timestamp) { '1707921703' }
  27. let(:json) do
  28. {
  29. object: 'whatsapp_business_account',
  30. entry: [{
  31. id: '222259550976437',
  32. changes: [{
  33. value: {
  34. messaging_product: 'whatsapp',
  35. metadata: {
  36. display_phone_number: '15551340563',
  37. phone_number_id: channel.options[:phone_number_id]
  38. },
  39. contacts: [{
  40. profile: {
  41. name: from[:name]
  42. },
  43. wa_id: from[:phone]
  44. }],
  45. messages: [{
  46. from: from[:phone],
  47. id: 'wamid.HBgNNDkxNTE1NjA4MDY5OBUCABIYFjNFQjBDMUM4M0I5NDRFNThBMUQyMjYA',
  48. timestamp: timestamp,
  49. text: {
  50. body: 'Hello, world!'
  51. },
  52. type: type
  53. }]
  54. },
  55. field: event
  56. }]
  57. }]
  58. }.to_json
  59. end
  60. let(:uuid) { channel.options[:callback_url_uuid] }
  61. let(:signature) do
  62. OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), channel.options[:app_secret], json)
  63. end
  64. describe '.new' do
  65. context 'when channel not exists' do
  66. let(:uuid) { 0 }
  67. it 'raises NoChannelError' do
  68. expect { described_class.new(json:, uuid:, signature:) }.to raise_error(Whatsapp::Webhook::NoChannelError)
  69. end
  70. end
  71. context 'when signatures do not match' do
  72. let(:signature) { 'foobar' }
  73. it 'raises ValidationError' do
  74. expect { described_class.new(json:, uuid:, signature:) }.to raise_error(described_class::ValidationError)
  75. end
  76. end
  77. context 'when signatures match' do
  78. it 'does not raise any error' do
  79. expect { described_class.new(json:, uuid:, signature:) }.not_to raise_error
  80. end
  81. end
  82. end
  83. describe '.process' do
  84. context 'when event is not messages' do
  85. let(:event) { 'foobar' }
  86. it 'raises ProcessableError' do
  87. expect { described_class.new(json:, uuid:, signature:).process }.to raise_error(described_class::ProcessableError)
  88. end
  89. end
  90. context 'when message has errors' do
  91. let(:json) do
  92. {
  93. object: 'whatsapp_business_account',
  94. entry: [{
  95. id: '222259550976437',
  96. changes: [{
  97. value: {
  98. messaging_product: 'whatsapp',
  99. metadata: {
  100. display_phone_number: '15551340563',
  101. phone_number_id: channel.options[:phone_number_id]
  102. },
  103. contacts: [{
  104. profile: {
  105. name: from[:name]
  106. },
  107. wa_id: from[:phone]
  108. }],
  109. messages: [{
  110. from: from[:phone],
  111. id: 'wamid.HBgNNDkxNTE1NjA4MDY5OBUCABIYFjNFQjBDMUM4M0I5NDRFNThBMUQyMjYA',
  112. timestamp: '1707921703',
  113. text: {
  114. body: 'Hello, world!'
  115. },
  116. errors: [
  117. {
  118. message: '(#130429) Rate limit hit',
  119. type: 'OAuthException',
  120. code: 130_429,
  121. error_data: {
  122. messaging_product: 'whatsapp',
  123. details: '<DETAILS>'
  124. },
  125. error_subcode: 2_494_055,
  126. fbtrace_id: 'Az8or2yhqkZfEZ-_4Qn_Bam'
  127. }
  128. ],
  129. type: type
  130. }]
  131. },
  132. field: 'messages'
  133. }]
  134. }]
  135. }.to_json
  136. end
  137. it 'raises ProcessableError' do
  138. expect { described_class.new(json:, uuid:, signature:).process }.to raise_error(described_class::ProcessableError)
  139. end
  140. end
  141. context 'when an unsupported type is used' do
  142. let(:type) { 'foobar' }
  143. it 'raises ProcessableError' do
  144. expect { described_class.new(json:, uuid:, signature:).process }.to raise_error(described_class::ProcessableError)
  145. end
  146. end
  147. context 'when everything is fine' do
  148. it 'does not raise any error' do
  149. expect { described_class.new(json:, uuid:, signature:).process }.not_to raise_error
  150. end
  151. context 'when no user exists' do
  152. it 'creates user' do
  153. described_class.new(json:, uuid:, signature:).process
  154. expect(User.last).to have_attributes(user_data)
  155. end
  156. end
  157. context 'when user already exists' do
  158. context 'when mobile is in common format with +' do
  159. before { create(:user, user_data) }
  160. it 'does not create a new user' do
  161. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(User, :count)
  162. end
  163. end
  164. context 'when mobile is in e164 format' do
  165. before { create(:user, user_data).tap { |u| u.update!(mobile: u.mobile.delete('+')) } }
  166. it 'does not create a new user' do
  167. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(User, :count)
  168. end
  169. end
  170. end
  171. context 'when no ticket exists' do
  172. it 'creates ticket' do
  173. expect { described_class.new(json:, uuid:, signature:).process }.to change(Ticket, :count).by(1)
  174. end
  175. it 'sets ticket preferences' do
  176. described_class.new(json:, uuid:, signature:).process
  177. expect(Ticket.last.preferences).to include(
  178. channel_id: channel.id,
  179. channel_area: channel.area,
  180. whatsapp: {
  181. from: {
  182. phone_number: from[:phone],
  183. display_name: from[:name],
  184. },
  185. timestamp_incoming: '1707921703',
  186. },
  187. )
  188. end
  189. end
  190. context 'when ticket already exists' do
  191. let(:ticket_state) { 'open' }
  192. let(:timestamp) { '1707921803' }
  193. let(:setup) do
  194. user = create(:user, user_data)
  195. create(:authorization, user: user, uid: user.mobile, provider: 'whatsapp_business')
  196. create(:ticket, customer: user, group_id: channel.group_id, state_id: Ticket::State.find_by(name: ticket_state).id, preferences: { channel_id: channel.id, channel_area: channel.area, whatsapp: { from: { phone_number: from[:phone], display_name: from[:name] }, timestamp_incoming: '1707921703' } })
  197. end
  198. before { setup }
  199. context 'when ticket is open' do
  200. it 'does not create a new ticket' do
  201. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(Ticket, :count)
  202. end
  203. it 'updates the ticket preferences' do
  204. described_class.new(json:, uuid:, signature:).process
  205. expect(Ticket.last.preferences).to include(
  206. channel_id: channel.id,
  207. channel_area: channel.area,
  208. whatsapp: {
  209. from: {
  210. phone_number: from[:phone],
  211. display_name: from[:name],
  212. },
  213. timestamp_incoming: '1707921803',
  214. },
  215. )
  216. end
  217. end
  218. context 'when ticket is closed' do
  219. let(:ticket_state) { 'closed' }
  220. it 'creates a new ticket' do
  221. expect { described_class.new(json:, uuid:, signature:).process }.to change(Ticket, :count).by(1)
  222. end
  223. end
  224. end
  225. end
  226. end
  227. describe '.process_status_message' do
  228. let(:channel) { create(:whatsapp_channel) }
  229. let(:from) do
  230. {
  231. phone: Faker::PhoneNumber.cell_phone_in_e164.delete('+'),
  232. name: Faker::Name.unique.name,
  233. }
  234. end
  235. let(:json) do
  236. {
  237. object: 'whatsapp_business_account',
  238. entry: [
  239. {
  240. id: '244742992051543',
  241. changes: [
  242. {
  243. value: {
  244. messaging_product: 'whatsapp',
  245. metadata: {
  246. display_phone_number: '15551340563',
  247. phone_number_id: channel.options[:phone_number_id],
  248. },
  249. statuses: [
  250. {
  251. id: message_id,
  252. status: 'failed',
  253. timestamp: '1708603746',
  254. recipient_id: '15551340563',
  255. errors: [
  256. {
  257. code: 131_047,
  258. title: 'Re-engagement message',
  259. message: 'Re-engagement message',
  260. error_data: {
  261. details: 'Message failed to send because more than 24 hours have passed since the customer last replied to this number.'
  262. },
  263. href: 'https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/'
  264. }
  265. ]
  266. }
  267. ]
  268. },
  269. field: 'messages'
  270. }
  271. ]
  272. }
  273. ]
  274. }.to_json
  275. end
  276. let(:article) { create(:whatsapp_article, :inbound, ticket: ticket) }
  277. let(:ticket) { create(:whatsapp_ticket, channel: channel) }
  278. let(:message_id) { article.message_id }
  279. let(:uuid) { channel.options[:callback_url_uuid] }
  280. let(:signature) do
  281. OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), channel.options[:app_secret], json)
  282. end
  283. context 'when all data is valid' do
  284. it 'creates a new record in the HttpLog' do
  285. described_class.new(json:, uuid:, signature:).process
  286. expect(HttpLog.last).to have_attributes(
  287. direction: 'in',
  288. facility: 'WhatsApp::Business',
  289. url: "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#{Rails.configuration.api_path}/channels_whatsapp_webhook/#{channel.options[:callback_url_uuid]}",
  290. status: '200',
  291. request: { content: JSON.parse(json).deep_symbolize_keys },
  292. response: { content: {} },
  293. method: 'POST',
  294. )
  295. end
  296. end
  297. end
  298. end