payload_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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. code: 0,
  119. details: 'Unable to authenticate the app user',
  120. title: 'AuthException'
  121. }
  122. ],
  123. type: type
  124. }]
  125. },
  126. field: 'messages'
  127. }]
  128. }]
  129. }.to_json
  130. end
  131. it 'raises ProcessableError' do
  132. expect { described_class.new(json:, uuid:, signature:).process }.to raise_error(described_class::ProcessableError)
  133. end
  134. it 'logs the error' do
  135. allow(Rails.logger).to receive(:error)
  136. begin
  137. described_class.new(json:, uuid:, signature:).process
  138. rescue
  139. # noop
  140. end
  141. expect(Rails.logger).to have_received(:error)
  142. .with("WhatsApp channel (#{channel.options[:callback_url_uuid]}) - failed message: AuthException (0)")
  143. end
  144. it 'updates the channel status' do
  145. begin
  146. described_class.new(json:, uuid:, signature:).process
  147. rescue
  148. # noop
  149. end
  150. expect(channel.reload).to have_attributes(
  151. status_out: 'error',
  152. last_log_out: 'AuthException (0)',
  153. )
  154. end
  155. end
  156. context 'when an unsupported type is used' do
  157. let(:type) { 'foobar' }
  158. it 'raises ProcessableError' do
  159. expect { described_class.new(json:, uuid:, signature:).process }.to raise_error(described_class::ProcessableError)
  160. end
  161. end
  162. context 'when everything is fine' do
  163. it 'does not raise any error' do
  164. expect { described_class.new(json:, uuid:, signature:).process }.not_to raise_error
  165. end
  166. context 'when no user exists' do
  167. it 'creates user' do
  168. described_class.new(json:, uuid:, signature:).process
  169. expect(User.last).to have_attributes(user_data)
  170. end
  171. end
  172. context 'when user already exists' do
  173. context 'when mobile is in common format with +' do
  174. before { create(:user, user_data) }
  175. it 'does not create a new user' do
  176. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(User, :count)
  177. end
  178. end
  179. context 'when mobile is in e164 format' do
  180. before { create(:user, user_data).tap { |u| u.update!(mobile: u.mobile.delete('+')) } }
  181. it 'does not create a new user' do
  182. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(User, :count)
  183. end
  184. end
  185. end
  186. context 'when no ticket exists' do
  187. it 'creates ticket' do
  188. expect { described_class.new(json:, uuid:, signature:).process }.to change(Ticket, :count).by(1)
  189. end
  190. it 'sets ticket preferences' do
  191. described_class.new(json:, uuid:, signature:).process
  192. expect(Ticket.last.preferences).to include(
  193. channel_id: channel.id,
  194. channel_area: channel.area,
  195. whatsapp: {
  196. from: {
  197. phone_number: from[:phone],
  198. display_name: from[:name],
  199. },
  200. timestamp_incoming: '1707921703',
  201. },
  202. )
  203. end
  204. end
  205. context 'when ticket already exists' do
  206. let(:ticket_state) { 'open' }
  207. let(:timestamp) { '1707921803' }
  208. let(:setup) do
  209. user = create(:user, user_data)
  210. create(:authorization, user: user, uid: user.mobile, provider: 'whatsapp_business')
  211. 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' } })
  212. end
  213. before { setup }
  214. context 'when ticket is open' do
  215. it 'does not create a new ticket' do
  216. expect { described_class.new(json:, uuid:, signature:).process }.not_to change(Ticket, :count)
  217. end
  218. it 'updates the ticket preferences' do
  219. described_class.new(json:, uuid:, signature:).process
  220. expect(Ticket.last.preferences).to include(
  221. channel_id: channel.id,
  222. channel_area: channel.area,
  223. whatsapp: {
  224. from: {
  225. phone_number: from[:phone],
  226. display_name: from[:name],
  227. },
  228. timestamp_incoming: '1707921803',
  229. },
  230. )
  231. end
  232. end
  233. context 'when ticket is closed' do
  234. let(:ticket_state) { 'closed' }
  235. it 'creates a new ticket' do
  236. expect { described_class.new(json:, uuid:, signature:).process }.to change(Ticket, :count).by(1)
  237. end
  238. end
  239. end
  240. end
  241. end
  242. describe '.process_status_message' do
  243. let(:channel) { create(:whatsapp_channel) }
  244. let(:from) do
  245. {
  246. phone: Faker::PhoneNumber.cell_phone_in_e164.delete('+'),
  247. name: Faker::Name.unique.name,
  248. }
  249. end
  250. let(:json) do
  251. {
  252. object: 'whatsapp_business_account',
  253. entry: [
  254. {
  255. id: '244742992051543',
  256. changes: [
  257. {
  258. value: {
  259. messaging_product: 'whatsapp',
  260. metadata: {
  261. display_phone_number: '15551340563',
  262. phone_number_id: channel.options[:phone_number_id],
  263. },
  264. statuses: [
  265. {
  266. id: message_id,
  267. status: 'failed',
  268. timestamp: '1708603746',
  269. recipient_id: '15551340563',
  270. errors: [
  271. {
  272. code: 131_047,
  273. title: 'Re-engagement message',
  274. message: 'Re-engagement message',
  275. error_data: {
  276. details: 'Message failed to send because more than 24 hours have passed since the customer last replied to this number.'
  277. },
  278. href: 'https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/'
  279. }
  280. ]
  281. }
  282. ]
  283. },
  284. field: 'messages'
  285. }
  286. ]
  287. }
  288. ]
  289. }.to_json
  290. end
  291. let(:article) { create(:whatsapp_article, :inbound, ticket: ticket) }
  292. let(:ticket) { create(:whatsapp_ticket, channel: channel) }
  293. let(:message_id) { article.message_id }
  294. let(:uuid) { channel.options[:callback_url_uuid] }
  295. let(:signature) do
  296. OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), channel.options[:app_secret], json)
  297. end
  298. context 'when all data is valid' do
  299. it 'creates a new record in the HttpLog' do
  300. described_class.new(json:, uuid:, signature:).process
  301. expect(HttpLog.last).to have_attributes(
  302. direction: 'in',
  303. facility: 'WhatsApp::Business',
  304. url: "#{Setting.get('http_type')}://#{Setting.get('fqdn')}/#{Rails.configuration.api_path}/channels_whatsapp_webhook/#{channel.options[:callback_url_uuid]}",
  305. status: '200',
  306. request: { content: JSON.parse(json).deep_symbolize_keys },
  307. response: { content: {} },
  308. method: 'POST',
  309. )
  310. end
  311. end
  312. end
  313. end