whatsapp_notifier 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,52 @@ RSpec.describe WhatsAppNotifier::WebAdapter do
24
24
  expect(result).to include(success: true, message_id: "k1")
25
25
  end
26
26
 
27
+ # The 0.8.0 service returns the real WhatsApp message id — the key the host
28
+ # dedupes the send's own fromMe echo on. It must beat the fabricated id.
29
+ it "prefers the service-issued message id over the idempotency key" do
30
+ response = http_success(body: { "success" => true, "messageId" => "true_919@c.us_ABC" })
31
+ allow(Net::HTTP).to receive(:start).and_return(response)
32
+
33
+ result = adapter.send_message(
34
+ payload: { to: "+1", body: "hi", metadata: { user_id: 1 }, idempotency_key: "k1" },
35
+ session: {}
36
+ )
37
+
38
+ expect(result).to include(success: true, message_id: "true_919@c.us_ABC")
39
+ end
40
+
41
+ it "accepts the snake_case message_id alias in the send response" do
42
+ response = http_success(body: { "success" => true, "message_id" => "true_919@c.us_DEF" })
43
+ allow(Net::HTTP).to receive(:start).and_return(response)
44
+
45
+ result = adapter.send_message(payload: { to: "+1", body: "hi", metadata: {} }, session: {})
46
+
47
+ expect(result).to include(message_id: "true_919@c.us_DEF")
48
+ end
49
+
50
+ # A 0.8.0 service that could not read the sent message's id answers
51
+ # messageId: null — fall through to the 0.7.0 fabrication chain.
52
+ it "falls back to the idempotency key when the service returns a null id" do
53
+ response = http_success(body: { "success" => true, "messageId" => nil })
54
+ allow(Net::HTTP).to receive(:start).and_return(response)
55
+
56
+ result = adapter.send_message(
57
+ payload: { to: "+1", body: "hi", metadata: {}, idempotency_key: "k1" },
58
+ session: {}
59
+ )
60
+
61
+ expect(result).to include(message_id: "k1")
62
+ end
63
+
64
+ it "fabricates a local id when neither the service nor the payload offers one" do
65
+ response = http_success(body: { "success" => true })
66
+ allow(Net::HTTP).to receive(:start).and_return(response)
67
+
68
+ result = adapter.send_message(payload: { to: "+1", body: "hi", metadata: {} }, session: {})
69
+
70
+ expect(result[:message_id]).to match(/\Alocal-\d+\z/)
71
+ end
72
+
27
73
  it "yields the http connection to run the request" do
28
74
  response = http_success(body: { "success" => true })
29
75
  http = instance_double(Net::HTTP, request: response)
@@ -100,6 +146,333 @@ RSpec.describe WhatsAppNotifier::WebAdapter do
100
146
  expect { adapter.fetch_inbound(metadata: {}) }.to raise_error(/service request failed/)
101
147
  end
102
148
 
149
+ it "maps the 0.7.0 media and sender keys when the wire payload carries them" do
150
+ body = { "messages" => [{
151
+ "from" => "919@c.us", "body" => "", "messageId" => "m1", "timestamp" => 123, "type" => "image",
152
+ "hasMedia" => true, "mediaStatus" => "available", "mediaMime" => "image/jpeg",
153
+ "mediaFilename" => "beach.jpg", "mediaSize" => 1024, "senderName" => "Asha"
154
+ }] }
155
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
156
+
157
+ message = adapter.fetch_inbound(metadata: { user_id: "u-1" }).first
158
+
159
+ expect(message).to include(
160
+ has_media: true, media_status: "available", media_mime: "image/jpeg",
161
+ media_filename: "beach.jpg", media_size: 1024, sender_name: "Asha"
162
+ )
163
+ end
164
+
165
+ it "accepts snake_case wire aliases and maps an unavailable verdict's error" do
166
+ body = { "messages" => [{
167
+ "from" => "919@c.us", "body" => "", "message_id" => "m1", "timestamp" => 1, "type" => "video",
168
+ "has_media" => true, "media_status" => "unavailable", "media_error" => "unsupported_type"
169
+ }] }
170
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
171
+
172
+ message = adapter.fetch_inbound(metadata: {}).first
173
+
174
+ expect(message).to include(has_media: true, media_status: "unavailable", media_error: "unsupported_type")
175
+ expect(message).not_to have_key(:media_mime)
176
+ end
177
+
178
+ # A 0.6.0 service sends no media keys at all — the mapped hash must omit
179
+ # them (not nil them), because hosts key-gate ingest on has_media presence.
180
+ # Same contract for the 0.8.0 two-way keys: a customer message carries no
181
+ # from_me/to, so they must be absent (hosts key-gate fromMe ingest too).
182
+ it "omits the media and two-way keys entirely for plain inbound payloads" do
183
+ body = [{ "from" => "919@c.us", "body" => "hi", "messageId" => "m1", "timestamp" => 123, "type" => "chat" }]
184
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
185
+
186
+ message = adapter.fetch_inbound(metadata: {}).first
187
+
188
+ expect(message.keys).to match_array(%i[from body message_id timestamp type])
189
+ end
190
+
191
+ # 0.8.0 two-way capture: operator-sent messages arrive with fromMe + to —
192
+ # `to` is the counterparty chat id the host threads the conversation on.
193
+ it "maps the 0.8.0 two-way keys when the wire payload carries them" do
194
+ body = { "messages" => [{
195
+ "from" => "919000000001@c.us", "to" => "919@c.us", "fromMe" => true,
196
+ "body" => "on my way", "messageId" => "true_919@c.us_OP1", "timestamp" => 5, "type" => "chat"
197
+ }] }
198
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
199
+
200
+ message = adapter.fetch_inbound(metadata: { user_id: "u-1" }).first
201
+
202
+ expect(message).to include(
203
+ from: "919000000001@c.us", to: "919@c.us", from_me: true,
204
+ body: "on my way", message_id: "true_919@c.us_OP1"
205
+ )
206
+ end
207
+
208
+ it "accepts the snake_case from_me wire alias" do
209
+ body = { "messages" => [{
210
+ "from" => "919000000001@c.us", "to" => "919@c.us", "from_me" => true,
211
+ "body" => "done", "message_id" => "m9", "timestamp" => 9, "type" => "chat"
212
+ }] }
213
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
214
+
215
+ message = adapter.fetch_inbound(metadata: {}).first
216
+
217
+ expect(message).to include(from_me: true, to: "919@c.us")
218
+ end
219
+
220
+ def binary_response(code:, body: "", headers: {})
221
+ response = double("binary response", code: code, body: body)
222
+ allow(response).to receive(:is_a?) { |klass| code == "200" && klass == Net::HTTPSuccess }
223
+ allow(response).to receive(:[]) { |name| headers[name] }
224
+ response
225
+ end
226
+
227
+ def run_binary_request(response)
228
+ captured = nil
229
+ http = double("http")
230
+ allow(http).to receive(:request) { |req| captured = req; response }
231
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
232
+ -> { captured }
233
+ end
234
+
235
+ it "fetches media bytes with mime, filename and size on a dedicated binary path" do
236
+ allow(ENV).to receive(:[]).and_call_original
237
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return(nil)
238
+ response = binary_response(
239
+ code: "200", body: "\xFF\xD8raw-jpeg".b,
240
+ headers: { "Content-Type" => "image/jpeg", "Content-Disposition" => 'attachment; filename="beach.jpg"' }
241
+ )
242
+ captured = run_binary_request(response)
243
+
244
+ media = adapter.fetch_media(message_id: "true_919@c.us_ABC", metadata: { user_id: "u-1" })
245
+
246
+ expect(media).to eq(body: "\xFF\xD8raw-jpeg".b, mime: "image/jpeg", filename: "beach.jpg", size: 10)
247
+ expect(captured.call.path).to eq("/media/u-1/true_919@c.us_ABC")
248
+ expect(captured.call["X-WA-Token"]).to be_nil # env unset -> no token header
249
+ end
250
+
251
+ it "returns nil when the service has no copy of the media (404)" do
252
+ allow(Net::HTTP).to receive(:start).and_return(binary_response(code: "404", body: '{"error":"not_found"}'))
253
+
254
+ expect(adapter.fetch_media(message_id: "m1", metadata: {})).to be_nil
255
+ end
256
+
257
+ it "raises on non-404 media fetch failures" do
258
+ allow(Net::HTTP).to receive(:start).and_return(binary_response(code: "500", body: "boom"))
259
+
260
+ expect { adapter.fetch_media(message_id: "m1", metadata: {}) }.to raise_error(/service request failed \(500\)/)
261
+ end
262
+
263
+ it "sends X-WA-Token on the media fetch when WHATSAPP_WEBHOOK_TOKEN is set" do
264
+ allow(ENV).to receive(:[]).and_call_original
265
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return("sekrit")
266
+ response = binary_response(code: "200", body: "x", headers: { "Content-Type" => "audio/ogg" })
267
+ captured = run_binary_request(response)
268
+
269
+ media = adapter.fetch_media(message_id: "m1", metadata: { user_id: "u-1" })
270
+
271
+ expect(captured.call["X-WA-Token"]).to eq("sekrit")
272
+ expect(media).to include(mime: "audio/ogg", filename: nil, size: 1)
273
+ end
274
+
275
+ it "strips path and query characters from the message id before building the URL" do
276
+ response = binary_response(code: "404")
277
+ captured = run_binary_request(response)
278
+
279
+ adapter.fetch_media(message_id: "../m1?x=1#f", metadata: { user_id: "u-1" })
280
+
281
+ expect(captured.call.path).to eq("/media/u-1/..m1x1f")
282
+ end
283
+
284
+ it "deletes media via the JSON control plane with the token attached" do
285
+ allow(ENV).to receive(:[]).and_call_original
286
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return("sekrit")
287
+ response = http_success(body: { "success" => true })
288
+ captured = nil
289
+ http = double("http")
290
+ allow(http).to receive(:request) { |req| captured = req; response }
291
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
292
+
293
+ expect(adapter.delete_media(message_id: "m/1", metadata: { user_id: "u-1" })).to eq(success: true)
294
+ expect(captured).to be_a(Net::HTTP::Delete)
295
+ expect(captured.path).to eq("/media/u-1/m1")
296
+ expect(captured["X-WA-Token"]).to eq("sekrit")
297
+ end
298
+
299
+ it "defaults delete_media success to false when the service omits it" do
300
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: {}))
301
+
302
+ expect(adapter.delete_media(message_id: "m1", metadata: {})).to eq(success: false)
303
+ end
304
+
305
+ # A 0.6.0 service mid-rollout has no /media routes — delete must degrade
306
+ # like fetch_media's nil-on-404, not raise.
307
+ it "returns success false when delete_media hits a 404" do
308
+ allow(Net::HTTP).to receive(:start)
309
+ .and_return(http_failure(code: "404", body: JSON.generate({ error: "not_found" })))
310
+
311
+ expect(adapter.delete_media(message_id: "m1", metadata: {})).to eq(success: false)
312
+ end
313
+
314
+ it "still raises when delete_media fails with a non-404 code" do
315
+ allow(Net::HTTP).to receive(:start)
316
+ .and_return(http_failure(code: "500", body: JSON.generate({ error: "boom" })))
317
+
318
+ expect { adapter.delete_media(message_id: "m1", metadata: {}) }
319
+ .to raise_error(/service request failed \(500\)/)
320
+ end
321
+
322
+ it "refetches media via POST, mapping the success verdict and attaching the token" do
323
+ allow(ENV).to receive(:[]).and_call_original
324
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return("sekrit")
325
+ response = http_success(body: {
326
+ "success" => true, "messageId" => "true_919@c.us_ABC", "mediaStatus" => "available",
327
+ "mediaMime" => "image/jpeg", "mediaFilename" => "beach.jpg", "mediaSize" => 10
328
+ })
329
+ captured = nil
330
+ http = double("http")
331
+ allow(http).to receive(:request) { |req| captured = req; response }
332
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
333
+
334
+ result = adapter.refetch_media(message_id: "true_919@c.us_ABC", chat_id: "919@c.us", metadata: { user_id: "u-1" })
335
+
336
+ expect(result).to eq(mime: "image/jpeg", filename: "beach.jpg", size: 10, status: "available")
337
+ expect(captured).to be_a(Net::HTTP::Post)
338
+ expect(captured.path).to eq("/media/u-1/refetch")
339
+ expect(captured["X-WA-Token"]).to eq("sekrit")
340
+ expect(JSON.parse(captured.body)).to eq("messageId" => "true_919@c.us_ABC", "chatId" => "919@c.us")
341
+ end
342
+
343
+ it "accepts the snake_case media keys in the refetch response" do
344
+ body = { "success" => true, "media_status" => "available", "media_mime" => "audio/ogg",
345
+ "media_filename" => "vn.ogg", "media_size" => 3 }
346
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
347
+
348
+ expect(adapter.refetch_media(message_id: "m1", chat_id: "919@c.us", metadata: {}))
349
+ .to eq(mime: "audio/ogg", filename: "vn.ogg", size: 3, status: "available")
350
+ end
351
+
352
+ # Media gone upstream → the service answers 404 success:false; refetch
353
+ # degrades to nil like fetch_media, so the host can grey the bubble out.
354
+ it "returns nil when the refetch reports the media is gone (404)" do
355
+ allow(Net::HTTP).to receive(:start)
356
+ .and_return(http_failure(code: "404", body: JSON.generate({ success: false, mediaStatus: "unavailable", mediaError: "gone" })))
357
+
358
+ expect(adapter.refetch_media(message_id: "m1", chat_id: "919@c.us", metadata: {})).to be_nil
359
+ end
360
+
361
+ # A success:false body that somehow arrives with a 2xx still degrades to nil.
362
+ it "returns nil when the refetch response is unsuccessful" do
363
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: { "success" => false, "mediaError" => "gone" }))
364
+
365
+ expect(adapter.refetch_media(message_id: "m1", chat_id: "919@c.us", metadata: {})).to be_nil
366
+ end
367
+
368
+ it "still raises when the refetch fails with a non-404 code" do
369
+ allow(Net::HTTP).to receive(:start)
370
+ .and_return(http_failure(code: "401", body: JSON.generate({ error: "User not authenticated" })))
371
+
372
+ expect { adapter.refetch_media(message_id: "m1", chat_id: "919@c.us", metadata: {}) }
373
+ .to raise_error(/service request failed \(401\)/)
374
+ end
375
+
376
+ it "lists chats with the token attached and maps the discovery keys" do
377
+ allow(ENV).to receive(:[]).and_call_original
378
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return("sekrit")
379
+ response = http_success(body: { "success" => true, "chats" => [
380
+ { "id" => "919@c.us", "name" => "Asha", "lastMessageAt" => 1_717_000_000 },
381
+ { "id" => "918@c.us", "name" => nil, "lastMessageAt" => nil }
382
+ ] })
383
+ captured = nil
384
+ http = double("http")
385
+ allow(http).to receive(:request) { |req| captured = req; response }
386
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
387
+
388
+ chats = adapter.list_chats(metadata: { user_id: "u-1" })
389
+
390
+ expect(chats).to eq([
391
+ { id: "919@c.us", name: "Asha", last_message_at: 1_717_000_000 },
392
+ { id: "918@c.us", name: nil, last_message_at: nil }
393
+ ])
394
+ expect(captured).to be_a(Net::HTTP::Get)
395
+ expect(captured.path).to eq("/chats/u-1")
396
+ expect(captured["X-WA-Token"]).to eq("sekrit")
397
+ end
398
+
399
+ it "accepts the snake_case last_message_at wire alias" do
400
+ body = { "chats" => [{ "id" => "919@c.us", "name" => "Asha", "last_message_at" => 9 }] }
401
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: body))
402
+
403
+ expect(adapter.list_chats(metadata: {}).first).to include(last_message_at: 9)
404
+ end
405
+
406
+ it "returns an empty chat list when the service omits the key" do
407
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: { "success" => true }))
408
+
409
+ expect(adapter.list_chats(metadata: {})).to eq([])
410
+ end
411
+
412
+ # An unpaired or not-ready user answers 401 — the standard non-2xx raise
413
+ # passes straight through to the caller.
414
+ it "raises the standard error when the chat list is unauthorized" do
415
+ allow(Net::HTTP).to receive(:start)
416
+ .and_return(http_failure(code: "401", body: JSON.generate({ error: "User not authenticated" })))
417
+
418
+ expect { adapter.list_chats(metadata: {}) }
419
+ .to raise_error(/service request failed \(401\): User not authenticated/)
420
+ end
421
+
422
+ it "fetches history with the token, posting the chat id and clamped limit" do
423
+ allow(ENV).to receive(:[]).and_call_original
424
+ allow(ENV).to receive(:[]).with("WHATSAPP_WEBHOOK_TOKEN").and_return("sekrit")
425
+ response = http_success(body: { "success" => true, "messages" => [
426
+ { "from" => "919@c.us", "body" => "old reply", "messageId" => "h1", "timestamp" => 1, "type" => "chat" },
427
+ { "from" => "919000000001@c.us", "to" => "919@c.us", "fromMe" => true,
428
+ "body" => "old send", "messageId" => "h2", "timestamp" => 2, "type" => "chat" },
429
+ { "from" => "919@c.us", "body" => "", "messageId" => "h3", "timestamp" => 3, "type" => "image",
430
+ "hasMedia" => true, "mediaStatus" => "unavailable", "mediaError" => "history" }
431
+ ] })
432
+ captured = nil
433
+ http = double("http")
434
+ allow(http).to receive(:request) { |req| captured = req; response }
435
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
436
+
437
+ messages = adapter.fetch_history(chat_id: "919@c.us", limit: 100_000, metadata: { user_id: "u-1" })
438
+
439
+ expect(captured).to be_a(Net::HTTP::Post)
440
+ expect(captured.path).to eq("/history/u-1")
441
+ expect(captured["X-WA-Token"]).to eq("sekrit")
442
+ expect(JSON.parse(captured.body)).to eq("chatId" => "919@c.us", "limit" => 200)
443
+
444
+ # Same mapper as fetch_inbound: two-way keys on the operator's messages,
445
+ # the by-design unavailable media verdict on history media.
446
+ expect(messages[0]).to eq(from: "919@c.us", body: "old reply", message_id: "h1", timestamp: 1, type: "chat")
447
+ expect(messages[1]).to include(from_me: true, to: "919@c.us", message_id: "h2")
448
+ expect(messages[2]).to include(has_media: true, media_status: "unavailable", media_error: "history")
449
+ end
450
+
451
+ it "clamps the history limit into 1..200 and defaults garbage to 50" do
452
+ bodies = []
453
+ http = double("http")
454
+ allow(http).to receive(:request) { |req| bodies << JSON.parse(req.body); http_success(body: { "messages" => [] }) }
455
+ allow(Net::HTTP).to receive(:start) { |*_args, **_kwargs, &blk| blk.call(http) }
456
+
457
+ adapter.fetch_history(chat_id: "919@c.us", metadata: {}) # default
458
+ adapter.fetch_history(chat_id: "919@c.us", limit: 0, metadata: {}) # below floor
459
+ adapter.fetch_history(chat_id: "919@c.us", limit: 201, metadata: {}) # above cap
460
+ adapter.fetch_history(chat_id: "919@c.us", limit: "120", metadata: {}) # numeric string
461
+ adapter.fetch_history(chat_id: "919@c.us", limit: 75.9, metadata: {}) # float floors
462
+ adapter.fetch_history(chat_id: "919@c.us", limit: "lots", metadata: {}) # garbage
463
+ adapter.fetch_history(chat_id: "919@c.us", limit: nil, metadata: {}) # nil
464
+
465
+ expect(bodies.map { |b| b["limit"] }).to eq([50, 1, 200, 120, 75, 50, 50])
466
+ end
467
+
468
+ it "raises the standard error when the history fetch fails" do
469
+ allow(Net::HTTP).to receive(:start)
470
+ .and_return(http_failure(code: "422", body: JSON.generate({ error: "`chatId` is required" })))
471
+
472
+ expect { adapter.fetch_history(chat_id: "12@g.us", metadata: {}) }
473
+ .to raise_error(/service request failed \(422\)/)
474
+ end
475
+
103
476
  it "logs out via the service" do
104
477
  allow(Net::HTTP).to receive(:start).and_return(http_success(body: { "success" => true }))
105
478
 
@@ -123,6 +496,40 @@ RSpec.describe WhatsAppNotifier::WebAdapter do
123
496
  expect(Net::HTTP::Get).to have_received(:new).with("/qr/default")
124
497
  end
125
498
 
499
+ # Net::HTTP does not infer TLS from the URL scheme — an https service URL
500
+ # without use_ssl would send the token and payloads in plaintext.
501
+ it "enables TLS for https service URLs on the JSON request path" do
502
+ secure = described_class.new(base_url: "https://wa.example.com")
503
+ allow(Net::HTTP).to receive(:start).and_return(http_success(body: { "success" => true }))
504
+
505
+ secure.logout(metadata: { user_id: "u-1" })
506
+
507
+ expect(Net::HTTP).to have_received(:start)
508
+ .with("wa.example.com", 443, hash_including(use_ssl: true))
509
+ end
510
+
511
+ it "enables TLS for https service URLs on the binary media path" do
512
+ secure = described_class.new(base_url: "https://wa.example.com")
513
+ allow(Net::HTTP).to receive(:start).and_return(binary_response(code: "404"))
514
+
515
+ secure.fetch_media(message_id: "m1", metadata: { user_id: "u-1" })
516
+
517
+ expect(Net::HTTP).to have_received(:start)
518
+ .with("wa.example.com", 443, hash_including(use_ssl: true))
519
+ end
520
+
521
+ it "keeps TLS off for plain http service URLs on both paths" do
522
+ allow(Net::HTTP).to receive(:start).and_return(
523
+ http_success(body: { "success" => true }), binary_response(code: "404")
524
+ )
525
+
526
+ adapter.logout(metadata: {})
527
+ adapter.fetch_media(message_id: "m1", metadata: {})
528
+
529
+ expect(Net::HTTP).to have_received(:start)
530
+ .with("127.0.0.1", 3001, hash_including(use_ssl: false)).twice
531
+ end
532
+
126
533
  it "executes the request inside the Net::HTTP block" do
127
534
  fake_http = instance_double(Net::HTTP)
128
535
  allow(fake_http).to receive(:request).and_return(http_success(body: { "qr" => "data:image/png;base64,x" }))
@@ -94,6 +94,11 @@ RSpec.describe WhatsAppNotifier do
94
94
  allow(fake_client).to receive(:scan_qr).and_return("qr-code")
95
95
  allow(fake_client).to receive(:connection_status).and_return(state: "QR_REQUIRED")
96
96
  allow(fake_client).to receive(:fetch_inbound).and_return([{ from: "q@c.us" }])
97
+ allow(fake_client).to receive(:fetch_media).and_return(body: "bytes", mime: "image/jpeg", filename: nil, size: 5)
98
+ allow(fake_client).to receive(:delete_media).and_return(success: true)
99
+ allow(fake_client).to receive(:refetch_media).and_return(mime: "image/jpeg", filename: nil, size: 5, status: "available")
100
+ allow(fake_client).to receive(:list_chats).and_return([{ id: "919@c.us", name: "Asha", last_message_at: 9 }])
101
+ allow(fake_client).to receive(:fetch_history).and_return([{ from: "919@c.us", body: "old", message_id: "h1" }])
97
102
  allow(fake_client).to receive(:logout).and_return(success: true)
98
103
  described_class.instance_variable_set(:@client, fake_client)
99
104
 
@@ -101,7 +106,35 @@ RSpec.describe WhatsAppNotifier do
101
106
  expect(described_class.scan_qr(provider: :web_automation, metadata: { user_id: 1 })).to eq("qr-code")
102
107
  expect(described_class.connection_status(provider: :web_automation, metadata: { user_id: 1 })).to include(state: "QR_REQUIRED")
103
108
  expect(described_class.fetch_inbound(provider: :web_automation, metadata: { user_id: 1 })).to eq([{ from: "q@c.us" }])
109
+ expect(described_class.fetch_media(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 })).to include(mime: "image/jpeg")
110
+ expect(described_class.delete_media(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 })).to eq(success: true)
111
+ expect(described_class.refetch_media(message_id: "m1", chat_id: "919@c.us", provider: :web_automation, metadata: { user_id: 1 })).to include(status: "available")
112
+ expect(described_class.list_chats(provider: :web_automation, metadata: { user_id: 1 })).to eq([{ id: "919@c.us", name: "Asha", last_message_at: 9 }])
113
+ expect(described_class.fetch_history(chat_id: "919@c.us", limit: 20, provider: :web_automation, metadata: { user_id: 1 })).to eq([{ from: "919@c.us", body: "old", message_id: "h1" }])
104
114
  expect(described_class.logout(provider: :web_automation, metadata: { user_id: 1 })).to eq(success: true)
115
+ expect(fake_client).to have_received(:fetch_media).with(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 })
116
+ expect(fake_client).to have_received(:delete_media).with(message_id: "m1", provider: :web_automation, metadata: { user_id: 1 })
117
+ expect(fake_client).to have_received(:refetch_media).with(message_id: "m1", chat_id: "919@c.us", provider: :web_automation, metadata: { user_id: 1 })
118
+ expect(fake_client).to have_received(:list_chats).with(provider: :web_automation, metadata: { user_id: 1 })
119
+ expect(fake_client).to have_received(:fetch_history).with(chat_id: "919@c.us", limit: 20, provider: :web_automation, metadata: { user_id: 1 })
120
+ end
121
+
122
+ it "fetches history through the module API end to end with the default limit" do
123
+ adapter = double(
124
+ send_message: { success: true, session: {} },
125
+ fetch_qr_code: "qr",
126
+ connection_status: { state: "AUTHENTICATED", authenticated: true }
127
+ )
128
+ allow(adapter).to receive(:fetch_history).and_return([{ from: "z@c.us", body: "old", message_id: "h1" }])
129
+ described_class.configure do |config|
130
+ config.provider = :web_automation
131
+ config.web_automation_enabled = true
132
+ config.web_adapter = adapter
133
+ end
134
+
135
+ expect(described_class.fetch_history(chat_id: "919@c.us", metadata: { user_id: 1 }))
136
+ .to eq([{ from: "z@c.us", body: "old", message_id: "h1" }])
137
+ expect(adapter).to have_received(:fetch_history).with(chat_id: "919@c.us", limit: 50, metadata: { user_id: 1 })
105
138
  end
106
139
 
107
140
  it "fetches inbound through the module API end to end" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: whatsapp_notifier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kshitiz Sinha
@@ -80,14 +80,20 @@ files:
80
80
  - lib/whatsapp_notifier/railtie.rb
81
81
  - lib/whatsapp_notifier/result.rb
82
82
  - lib/whatsapp_notifier/services/web_automation/bun.lock
83
+ - lib/whatsapp_notifier/services/web_automation/history.test.ts
84
+ - lib/whatsapp_notifier/services/web_automation/history.ts
83
85
  - lib/whatsapp_notifier/services/web_automation/inbound.test.ts
84
86
  - lib/whatsapp_notifier/services/web_automation/inbound.ts
85
87
  - lib/whatsapp_notifier/services/web_automation/index.ts
86
88
  - lib/whatsapp_notifier/services/web_automation/init_gate.test.ts
87
89
  - lib/whatsapp_notifier/services/web_automation/init_gate.ts
90
+ - lib/whatsapp_notifier/services/web_automation/media.test.ts
91
+ - lib/whatsapp_notifier/services/web_automation/media.ts
88
92
  - lib/whatsapp_notifier/services/web_automation/metrics.test.ts
89
93
  - lib/whatsapp_notifier/services/web_automation/metrics.ts
90
94
  - lib/whatsapp_notifier/services/web_automation/package.json
95
+ - lib/whatsapp_notifier/services/web_automation/send.test.ts
96
+ - lib/whatsapp_notifier/services/web_automation/send.ts
91
97
  - lib/whatsapp_notifier/services/web_automation/sessions.test.ts
92
98
  - lib/whatsapp_notifier/services/web_automation/sessions.ts
93
99
  - lib/whatsapp_notifier/session/qr_service.rb