kirimi 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/kirimi.gemspec +3 -1
- data/lib/kirimi/client.rb +157 -46
- data/lib/kirimi/version.rb +1 -1
- data/spec/client_spec.rb +817 -0
- metadata +36 -8
- data/test/client_test.rb +0 -160
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f1ffed06942da09afddf27319d189d03aa8230e6f711b7cd8b5e7fabc553507
|
|
4
|
+
data.tar.gz: 477551b4134d8fda91be2ac6e7428ec76ccc19776135f8c44d653db3c9ec40c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab06658c77c3c00460c80750702841282caea944b4c2bdfdada7d8720a8ccfe2c7422a272b7e6350dd600a75e8401513f85319b858c36afa8d0807a6386f44fa
|
|
7
|
+
data.tar.gz: d8f1c9079ae6457760ccddedb4e84213bad075219fd55f006803e043292a646734d0c9117a076fe45c6af443315f6c8d7b40d7fff1be7324ac508d765deb29ae
|
data/kirimi.gemspec
CHANGED
|
@@ -20,10 +20,12 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
21
21
|
|
|
22
22
|
spec.files = Dir.glob(%w[lib/**/* LICENSE README.md CHANGELOG.md kirimi.gemspec])
|
|
23
|
-
spec.test_files = Dir.glob('
|
|
23
|
+
spec.test_files = Dir.glob('spec/**/*_spec.rb')
|
|
24
24
|
spec.require_paths = ['lib']
|
|
25
25
|
|
|
26
26
|
# No runtime dependencies — uses only Ruby stdlib (net/http, json, securerandom)
|
|
27
27
|
|
|
28
28
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
29
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
|
30
|
+
spec.add_development_dependency 'webmock', '~> 3.19'
|
|
29
31
|
end
|
data/lib/kirimi/client.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'uri'
|
|
6
|
+
require 'securerandom'
|
|
6
7
|
|
|
7
8
|
module Kirimi
|
|
8
9
|
class Client
|
|
@@ -18,43 +19,122 @@ module Kirimi
|
|
|
18
19
|
|
|
19
20
|
# --- WhatsApp Unofficial ---
|
|
20
21
|
|
|
21
|
-
def send_message(device_id:,
|
|
22
|
-
|
|
23
|
-
body
|
|
22
|
+
def send_message(device_id:, receiver:, message:, media_url: nil, file_name: nil,
|
|
23
|
+
enable_typing_effect: nil, typing_speed_ms: nil, quoted_message_id: nil)
|
|
24
|
+
body = { device_id: device_id, receiver: receiver, message: message }
|
|
25
|
+
body[:media_url] = media_url if media_url
|
|
26
|
+
body[:fileName] = file_name if file_name
|
|
27
|
+
body[:enableTypingEffect] = enable_typing_effect unless enable_typing_effect.nil?
|
|
28
|
+
body[:typingSpeedMs] = typing_speed_ms if typing_speed_ms
|
|
29
|
+
body[:quotedMessageId] = quoted_message_id if quoted_message_id
|
|
24
30
|
post('/v1/send-message', body)
|
|
25
31
|
end
|
|
26
32
|
|
|
27
|
-
def send_message_file(device_id:,
|
|
33
|
+
def send_message_file(device_id:, receiver:, file:, file_name: nil, message: nil, caption: nil,
|
|
34
|
+
quoted_message_id: nil)
|
|
28
35
|
file_io = file.is_a?(String) ? File.open(file, 'rb') : file
|
|
29
36
|
fields = {
|
|
30
37
|
'user_code' => @user_code,
|
|
31
38
|
'secret' => @secret,
|
|
32
39
|
'device_id' => device_id,
|
|
33
|
-
'
|
|
34
|
-
'fileName' => file_name
|
|
40
|
+
'receiver' => receiver
|
|
35
41
|
}
|
|
36
|
-
fields['
|
|
37
|
-
|
|
42
|
+
fields['fileName'] = file_name if file_name
|
|
43
|
+
fields['message'] = message if message
|
|
44
|
+
fields['caption'] = caption if caption
|
|
45
|
+
fields['quotedMessageId'] = quoted_message_id if quoted_message_id
|
|
46
|
+
post_multipart('/v1/send-message-file', fields, file_io, file_name || 'file')
|
|
38
47
|
ensure
|
|
39
48
|
file_io&.close if file.is_a?(String)
|
|
40
49
|
end
|
|
41
50
|
|
|
42
|
-
def send_message_fast(device_id:,
|
|
43
|
-
|
|
44
|
-
body
|
|
51
|
+
def send_message_fast(device_id:, receiver:, message:, media_url: nil, file_name: nil,
|
|
52
|
+
quoted_message_id: nil)
|
|
53
|
+
body = { device_id: device_id, receiver: receiver, message: message }
|
|
54
|
+
body[:media_url] = media_url if media_url
|
|
55
|
+
body[:fileName] = file_name if file_name
|
|
56
|
+
body[:quotedMessageId] = quoted_message_id if quoted_message_id
|
|
45
57
|
post('/v1/send-message-fast', body)
|
|
46
58
|
end
|
|
47
59
|
|
|
48
|
-
# ---
|
|
60
|
+
# --- Broadcast ---
|
|
61
|
+
|
|
62
|
+
def broadcast_message(device_id:, label:, numbers:, message:, delay: nil, delay_min: nil,
|
|
63
|
+
delay_max: nil, media_url: nil, file_name: nil, started_at: nil,
|
|
64
|
+
enable_typing_effect: nil, typing_speed_ms: nil)
|
|
65
|
+
body = {
|
|
66
|
+
device_id: device_id,
|
|
67
|
+
label: label,
|
|
68
|
+
numbers: Array(numbers),
|
|
69
|
+
message: message
|
|
70
|
+
}
|
|
71
|
+
body[:delay] = delay if delay
|
|
72
|
+
body[:delayMin] = delay_min if delay_min
|
|
73
|
+
body[:delayMax] = delay_max if delay_max
|
|
74
|
+
body[:media_url] = media_url if media_url
|
|
75
|
+
body[:fileName] = file_name if file_name
|
|
76
|
+
body[:started_at] = started_at if started_at
|
|
77
|
+
body[:enableTypingEffect] = enable_typing_effect unless enable_typing_effect.nil?
|
|
78
|
+
body[:typingSpeedMs] = typing_speed_ms if typing_speed_ms
|
|
79
|
+
post('/v1/broadcast-message', body)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# --- WABA (Cloud API) ---
|
|
83
|
+
|
|
84
|
+
def send_waba_message(waba_id:, to:, template_name:, variables: nil, header: nil, buttons: nil)
|
|
85
|
+
body = { waba_id: waba_id, to: to, template_name: template_name }
|
|
86
|
+
body[:variables] = variables if variables
|
|
87
|
+
body[:header] = header if header
|
|
88
|
+
body[:buttons] = buttons if buttons
|
|
89
|
+
post('/v1/waba/send-message', body)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def waba_reply(waba_id:, to:, message:)
|
|
93
|
+
post('/v1/waba/messages/reply', { waba_id: waba_id, to: to, message: message })
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def waba_conversations(limit: nil, page: nil)
|
|
97
|
+
body = {}
|
|
98
|
+
body[:limit] = limit if limit
|
|
99
|
+
body[:page] = page if page
|
|
100
|
+
post('/v1/waba/conversations', body)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def waba_templates_sync(waba_id:)
|
|
104
|
+
post('/v1/waba/templates/sync', { waba_id: waba_id })
|
|
105
|
+
end
|
|
49
106
|
|
|
50
|
-
def
|
|
51
|
-
post('/v1/waba/send-
|
|
107
|
+
def waba_send_otp(waba_id:, to:, template_name:)
|
|
108
|
+
post('/v1/waba/send-otp', { waba_id: waba_id, to: to, template_name: template_name })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def waba_verify_otp(waba_id:, to:, otp_code:)
|
|
112
|
+
post('/v1/waba/verify-otp', { waba_id: waba_id, to: to, otp_code: otp_code })
|
|
52
113
|
end
|
|
53
114
|
|
|
54
115
|
# --- Devices ---
|
|
55
116
|
|
|
56
|
-
def
|
|
57
|
-
|
|
117
|
+
def create_device(package_id:, voucher_code: nil)
|
|
118
|
+
body = { package_id: package_id }
|
|
119
|
+
body[:voucher_code] = voucher_code if voucher_code
|
|
120
|
+
post('/v1/create-device', body)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def connect_device(device_id:)
|
|
124
|
+
post('/v1/connect-device', { device_id: device_id })
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def renew_device(device_id:, package_id:, voucher_code: nil)
|
|
128
|
+
body = { device_id: device_id, package_id: package_id }
|
|
129
|
+
body[:voucher_code] = voucher_code if voucher_code
|
|
130
|
+
post('/v1/renew-device', body)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def list_devices(page: nil, limit: nil)
|
|
134
|
+
body = {}
|
|
135
|
+
body[:page] = page if page
|
|
136
|
+
body[:limit] = limit if limit
|
|
137
|
+
post('/v1/list-devices', body)
|
|
58
138
|
end
|
|
59
139
|
|
|
60
140
|
def device_status(device_id:)
|
|
@@ -73,20 +153,29 @@ module Kirimi
|
|
|
73
153
|
|
|
74
154
|
# --- Contacts ---
|
|
75
155
|
|
|
76
|
-
def save_contact(
|
|
77
|
-
body = {
|
|
78
|
-
body[:
|
|
79
|
-
body[:email] = email if email
|
|
156
|
+
def save_contact(nama:, nomor:, device_id: nil)
|
|
157
|
+
body = { nama: nama, nomor: nomor }
|
|
158
|
+
body[:device_id] = device_id if device_id
|
|
80
159
|
post('/v1/save-contact', body)
|
|
81
160
|
end
|
|
82
161
|
|
|
83
|
-
|
|
162
|
+
def save_contacts_bulk(contacts:, device_id: nil)
|
|
163
|
+
body = { contacts: contacts }
|
|
164
|
+
body[:device_id] = device_id if device_id
|
|
165
|
+
post('/v1/save-contacts-bulk', body)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# --- OTP (V1) ---
|
|
84
169
|
|
|
85
|
-
def generate_otp(device_id:, phone:, otp_length: nil, otp_type: nil,
|
|
170
|
+
def generate_otp(device_id:, phone:, otp_length: nil, otp_type: nil, custom_otp_text: nil,
|
|
171
|
+
custom_otp_message: nil, enable_typing_effect: nil, typing_speed_ms: nil)
|
|
86
172
|
body = { device_id: device_id, phone: phone }
|
|
87
|
-
body[:otp_length]
|
|
88
|
-
body[:otp_type]
|
|
89
|
-
body[:
|
|
173
|
+
body[:otp_length] = otp_length if otp_length
|
|
174
|
+
body[:otp_type] = otp_type if otp_type
|
|
175
|
+
body[:customOtpText] = custom_otp_text if custom_otp_text
|
|
176
|
+
body[:customOtpMessage] = custom_otp_message if custom_otp_message
|
|
177
|
+
body[:enableTypingEffect] = enable_typing_effect unless enable_typing_effect.nil?
|
|
178
|
+
body[:typingSpeedMs] = typing_speed_ms if typing_speed_ms
|
|
90
179
|
post('/v1/generate-otp', body)
|
|
91
180
|
end
|
|
92
181
|
|
|
@@ -94,13 +183,16 @@ module Kirimi
|
|
|
94
183
|
post('/v1/validate-otp', { device_id: device_id, phone: phone, otp: otp })
|
|
95
184
|
end
|
|
96
185
|
|
|
97
|
-
# --- OTP V2 ---
|
|
186
|
+
# --- OTP (V2) ---
|
|
98
187
|
|
|
99
|
-
def send_otp_v2(phone:,
|
|
100
|
-
|
|
101
|
-
body
|
|
102
|
-
body[:
|
|
103
|
-
body[:
|
|
188
|
+
def send_otp_v2(phone:, method: nil, app_name: nil, device_id: nil, waba_id: nil,
|
|
189
|
+
template_name: nil, custom_message: nil)
|
|
190
|
+
body = { phone: phone }
|
|
191
|
+
body[:method] = method if method
|
|
192
|
+
body[:app_name] = app_name if app_name
|
|
193
|
+
body[:device_id] = device_id if device_id
|
|
194
|
+
body[:waba_id] = waba_id if waba_id
|
|
195
|
+
body[:template_name] = template_name if template_name
|
|
104
196
|
body[:custom_message] = custom_message if custom_message
|
|
105
197
|
post('/v2/otp/send', body)
|
|
106
198
|
end
|
|
@@ -109,27 +201,49 @@ module Kirimi
|
|
|
109
201
|
post('/v2/otp/verify', { phone: phone, otp_code: otp_code })
|
|
110
202
|
end
|
|
111
203
|
|
|
112
|
-
# ---
|
|
204
|
+
# --- OTP Reverse ---
|
|
113
205
|
|
|
114
|
-
def
|
|
115
|
-
|
|
116
|
-
body = {
|
|
117
|
-
body[:
|
|
118
|
-
|
|
206
|
+
def otp_reverse_create(phone:, device_id:, app_name: nil, callback_url: nil,
|
|
207
|
+
custom_message: nil, success_message: nil, failure_message: nil)
|
|
208
|
+
body = { phone: phone, device_id: device_id }
|
|
209
|
+
body[:app_name] = app_name if app_name
|
|
210
|
+
body[:callback_url] = callback_url if callback_url
|
|
211
|
+
body[:custom_message] = custom_message if custom_message
|
|
212
|
+
body[:success_message] = success_message if success_message
|
|
213
|
+
body[:failure_message] = failure_message if failure_message
|
|
214
|
+
post('/v2/otp-reverse/create', body)
|
|
119
215
|
end
|
|
120
216
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def list_deposits(status: nil)
|
|
124
|
-
body = {}
|
|
125
|
-
body[:status] = status if status
|
|
126
|
-
post('/v1/list-deposits', body)
|
|
217
|
+
def otp_reverse_status(token:)
|
|
218
|
+
post('/v2/otp-reverse/status', { token: token })
|
|
127
219
|
end
|
|
128
220
|
|
|
221
|
+
# --- Packages & Deposits ---
|
|
222
|
+
|
|
129
223
|
def list_packages
|
|
130
224
|
post('/v1/list-packages', {})
|
|
131
225
|
end
|
|
132
226
|
|
|
227
|
+
def create_deposit(nominal:)
|
|
228
|
+
post('/v1/create-deposit', { nominal: nominal })
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def deposit_status(ref:)
|
|
232
|
+
post('/v1/deposit-status', { ref: ref })
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def cancel_deposit(ref:)
|
|
236
|
+
post('/v1/cancel-deposit', { ref: ref })
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def list_deposits(page: nil, limit: nil, status: nil)
|
|
240
|
+
body = {}
|
|
241
|
+
body[:page] = page if page
|
|
242
|
+
body[:limit] = limit if limit
|
|
243
|
+
body[:status] = status if status
|
|
244
|
+
post('/v1/list-deposits', body)
|
|
245
|
+
end
|
|
246
|
+
|
|
133
247
|
private
|
|
134
248
|
|
|
135
249
|
def auth_params
|
|
@@ -213,6 +327,3 @@ module Kirimi
|
|
|
213
327
|
end
|
|
214
328
|
end
|
|
215
329
|
end
|
|
216
|
-
|
|
217
|
-
# Lazy require SecureRandom (part of stdlib, always available)
|
|
218
|
-
require 'securerandom'
|
data/lib/kirimi/version.rb
CHANGED
data/spec/client_spec.rb
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Kirimi::Client do
|
|
4
|
+
let(:user_code) { 'TEST_USER' }
|
|
5
|
+
let(:secret) { 'TEST_SECRET' }
|
|
6
|
+
let(:client) { described_class.new(user_code: user_code, secret: secret) }
|
|
7
|
+
let(:base_url) { 'https://api.kirimi.id' }
|
|
8
|
+
|
|
9
|
+
OK = { 'success' => true, 'data' => nil, 'message' => 'ok' }.freeze
|
|
10
|
+
|
|
11
|
+
# Stub a POST and assert the exact JSON body sent on the wire.
|
|
12
|
+
def stub_post_exact(path, expected_body, response_body: OK, status: 200)
|
|
13
|
+
stub_request(:post, "#{base_url}#{path}")
|
|
14
|
+
.with(
|
|
15
|
+
body: expected_body,
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type' => 'application/json',
|
|
18
|
+
'Accept' => 'application/json'
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
.to_return(
|
|
22
|
+
status: status,
|
|
23
|
+
body: response_body.to_json,
|
|
24
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def auth
|
|
29
|
+
{ 'user_code' => user_code, 'secret' => secret }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Stub a POST; the returned holder's #body holds the parsed JSON body sent.
|
|
33
|
+
def capture_post(path, response_body: OK, status: 200)
|
|
34
|
+
holder = Struct.new(:body).new(nil)
|
|
35
|
+
stub_request(:post, "#{base_url}#{path}")
|
|
36
|
+
.with do |req|
|
|
37
|
+
holder.body = JSON.parse(req.body)
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
.to_return(
|
|
41
|
+
status: status,
|
|
42
|
+
body: response_body.to_json,
|
|
43
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
44
|
+
)
|
|
45
|
+
holder
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# --- WhatsApp Unofficial ---
|
|
49
|
+
|
|
50
|
+
describe '#send_message' do
|
|
51
|
+
it 'sends receiver (never phone) and returns a Response' do
|
|
52
|
+
stub_post_exact(
|
|
53
|
+
'/v1/send-message',
|
|
54
|
+
auth.merge(
|
|
55
|
+
'device_id' => 'DEV1',
|
|
56
|
+
'receiver' => '628111222333',
|
|
57
|
+
'message' => 'Hello!'
|
|
58
|
+
),
|
|
59
|
+
response_body: { 'success' => true, 'data' => { 'id' => 'msg_1' }, 'message' => 'Message sent' }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
resp = client.send_message(device_id: 'DEV1', receiver: '628111222333', message: 'Hello!')
|
|
63
|
+
|
|
64
|
+
expect(resp).to be_a(Kirimi::Response)
|
|
65
|
+
expect(resp.success?).to be true
|
|
66
|
+
expect(resp.data).to eq('id' => 'msg_1')
|
|
67
|
+
expect(resp.message).to eq('Message sent')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'never sends a phone key' do
|
|
71
|
+
captured = capture_post('/v1/send-message')
|
|
72
|
+
|
|
73
|
+
client.send_message(device_id: 'DEV1', receiver: '628111222333', message: 'Hi')
|
|
74
|
+
|
|
75
|
+
expect(captured.body).not_to have_key('phone')
|
|
76
|
+
expect(captured.body['receiver']).to eq('628111222333')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'includes all optional params when provided' do
|
|
80
|
+
stub_post_exact(
|
|
81
|
+
'/v1/send-message',
|
|
82
|
+
auth.merge(
|
|
83
|
+
'device_id' => 'DEV1',
|
|
84
|
+
'receiver' => '628111',
|
|
85
|
+
'message' => 'Check this',
|
|
86
|
+
'media_url' => 'https://example.com/img.jpg',
|
|
87
|
+
'fileName' => 'img.jpg',
|
|
88
|
+
'enableTypingEffect' => true,
|
|
89
|
+
'typingSpeedMs' => 350,
|
|
90
|
+
'quotedMessageId' => 'wamid.1'
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
resp = client.send_message(
|
|
95
|
+
device_id: 'DEV1',
|
|
96
|
+
receiver: '628111',
|
|
97
|
+
message: 'Check this',
|
|
98
|
+
media_url: 'https://example.com/img.jpg',
|
|
99
|
+
file_name: 'img.jpg',
|
|
100
|
+
enable_typing_effect: true,
|
|
101
|
+
typing_speed_ms: 350,
|
|
102
|
+
quoted_message_id: 'wamid.1'
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
expect(resp.success?).to be true
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'omits optional params when nil' do
|
|
109
|
+
captured = capture_post('/v1/send-message')
|
|
110
|
+
|
|
111
|
+
client.send_message(device_id: 'DEV1', receiver: '628111', message: 'Hi')
|
|
112
|
+
|
|
113
|
+
expect(captured.body).to eq(
|
|
114
|
+
auth.merge('device_id' => 'DEV1', 'receiver' => '628111', 'message' => 'Hi')
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'keeps enableTypingEffect false rather than dropping it' do
|
|
119
|
+
captured = capture_post('/v1/send-message')
|
|
120
|
+
|
|
121
|
+
client.send_message(device_id: 'DEV1', receiver: '628111', message: 'Hi',
|
|
122
|
+
enable_typing_effect: false)
|
|
123
|
+
|
|
124
|
+
expect(captured.body).to have_key('enableTypingEffect')
|
|
125
|
+
expect(captured.body['enableTypingEffect']).to be false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
describe '#send_message_fast' do
|
|
130
|
+
it 'sends receiver and omits typing-effect params' do
|
|
131
|
+
captured = capture_post('/v1/send-message-fast')
|
|
132
|
+
|
|
133
|
+
client.send_message_fast(
|
|
134
|
+
device_id: 'DEV1',
|
|
135
|
+
receiver: '628111',
|
|
136
|
+
message: 'Fast',
|
|
137
|
+
media_url: 'https://example.com/a.jpg',
|
|
138
|
+
file_name: 'a.jpg',
|
|
139
|
+
quoted_message_id: 'wamid.2'
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
expect(captured.body).to eq(
|
|
143
|
+
auth.merge(
|
|
144
|
+
'device_id' => 'DEV1',
|
|
145
|
+
'receiver' => '628111',
|
|
146
|
+
'message' => 'Fast',
|
|
147
|
+
'media_url' => 'https://example.com/a.jpg',
|
|
148
|
+
'fileName' => 'a.jpg',
|
|
149
|
+
'quotedMessageId' => 'wamid.2'
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
expect(captured.body).not_to have_key('phone')
|
|
153
|
+
expect(captured.body).not_to have_key('enableTypingEffect')
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe '#send_message_file' do
|
|
158
|
+
let(:tmpfile) { Tempfile.new(['kirimi', '.txt']) }
|
|
159
|
+
|
|
160
|
+
after { tmpfile.close! }
|
|
161
|
+
|
|
162
|
+
it 'sends receiver instead of phone in the multipart body' do
|
|
163
|
+
stub_request(:post, "#{base_url}/v1/send-message-file").to_return(
|
|
164
|
+
status: 200,
|
|
165
|
+
body: OK.to_json,
|
|
166
|
+
headers: { 'Content-Type' => 'application/json' }
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
resp = client.send_message_file(
|
|
170
|
+
device_id: 'DEV1',
|
|
171
|
+
receiver: '628111',
|
|
172
|
+
file: tmpfile.path,
|
|
173
|
+
file_name: 'notes.txt',
|
|
174
|
+
message: 'caption text'
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
expect(resp.success?).to be true
|
|
178
|
+
expect(
|
|
179
|
+
a_request(:post, "#{base_url}/v1/send-message-file")
|
|
180
|
+
.with { |req| req.body.include?('name="receiver"') && !req.body.include?('name="phone"') }
|
|
181
|
+
).to have_been_made.once
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
describe '#broadcast_message' do
|
|
186
|
+
it 'sends numbers as an Array and requires label' do
|
|
187
|
+
captured = capture_post('/v1/broadcast-message')
|
|
188
|
+
|
|
189
|
+
client.broadcast_message(
|
|
190
|
+
device_id: 'DEV1',
|
|
191
|
+
label: 'promo-juli',
|
|
192
|
+
numbers: %w[628111 628222 628333],
|
|
193
|
+
message: 'Promo!'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(captured.body).to eq(
|
|
197
|
+
auth.merge(
|
|
198
|
+
'device_id' => 'DEV1',
|
|
199
|
+
'label' => 'promo-juli',
|
|
200
|
+
'numbers' => %w[628111 628222 628333],
|
|
201
|
+
'message' => 'Promo!'
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
expect(captured.body['numbers']).to be_an(Array)
|
|
205
|
+
expect(captured.body).not_to have_key('phones')
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'does not join numbers into a comma string' do
|
|
209
|
+
captured = capture_post('/v1/broadcast-message')
|
|
210
|
+
|
|
211
|
+
client.broadcast_message(device_id: 'DEV1', label: 'x', numbers: %w[628111 628222], message: 'Hi')
|
|
212
|
+
|
|
213
|
+
expect(captured.body['numbers']).to eq(%w[628111 628222])
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'includes optional scheduling params' do
|
|
217
|
+
captured = capture_post('/v1/broadcast-message')
|
|
218
|
+
|
|
219
|
+
client.broadcast_message(
|
|
220
|
+
device_id: 'DEV1',
|
|
221
|
+
label: 'sched',
|
|
222
|
+
numbers: %w[628111],
|
|
223
|
+
message: 'Later',
|
|
224
|
+
delay: 60,
|
|
225
|
+
delay_min: 30,
|
|
226
|
+
delay_max: 3600,
|
|
227
|
+
media_url: 'https://example.com/p.jpg',
|
|
228
|
+
file_name: 'p.jpg',
|
|
229
|
+
started_at: '2026-01-01T00:00:00Z',
|
|
230
|
+
enable_typing_effect: true,
|
|
231
|
+
typing_speed_ms: 200
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
expect(captured.body).to include(
|
|
235
|
+
'delay' => 60,
|
|
236
|
+
'delayMin' => 30,
|
|
237
|
+
'delayMax' => 3600,
|
|
238
|
+
'media_url' => 'https://example.com/p.jpg',
|
|
239
|
+
'fileName' => 'p.jpg',
|
|
240
|
+
'started_at' => '2026-01-01T00:00:00Z',
|
|
241
|
+
'enableTypingEffect' => true,
|
|
242
|
+
'typingSpeedMs' => 200
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# --- WABA ---
|
|
248
|
+
|
|
249
|
+
describe '#send_waba_message' do
|
|
250
|
+
it 'sends waba_id, to and template_name' do
|
|
251
|
+
captured = capture_post('/v1/waba/send-message')
|
|
252
|
+
|
|
253
|
+
client.send_waba_message(
|
|
254
|
+
waba_id: 'WABA1',
|
|
255
|
+
to: '628111',
|
|
256
|
+
template_name: 'order_confirmation'
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
expect(captured.body).to eq(
|
|
260
|
+
auth.merge('waba_id' => 'WABA1', 'to' => '628111', 'template_name' => 'order_confirmation')
|
|
261
|
+
)
|
|
262
|
+
expect(captured.body).not_to have_key('device_id')
|
|
263
|
+
expect(captured.body).not_to have_key('phone')
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it 'includes variables, header and buttons' do
|
|
267
|
+
captured = capture_post('/v1/waba/send-message')
|
|
268
|
+
|
|
269
|
+
client.send_waba_message(
|
|
270
|
+
waba_id: 'WABA1',
|
|
271
|
+
to: '628111',
|
|
272
|
+
template_name: 'promo',
|
|
273
|
+
variables: %w[Alice 100],
|
|
274
|
+
header: { 'type' => 'text', 'text' => 'Hi' },
|
|
275
|
+
buttons: [{ 'type' => 'url', 'url' => 'https://x.test' }]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
expect(captured.body['variables']).to eq(%w[Alice 100])
|
|
279
|
+
expect(captured.body['header']).to eq('type' => 'text', 'text' => 'Hi')
|
|
280
|
+
expect(captured.body['buttons']).to eq([{ 'type' => 'url', 'url' => 'https://x.test' }])
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'omits optional fields when nil' do
|
|
284
|
+
captured = capture_post('/v1/waba/send-message')
|
|
285
|
+
|
|
286
|
+
client.send_waba_message(waba_id: 'WABA1', to: '628111', template_name: 't')
|
|
287
|
+
|
|
288
|
+
expect(captured.body.keys).to contain_exactly('user_code', 'secret', 'waba_id', 'to', 'template_name')
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
describe '#waba_reply' do
|
|
293
|
+
it 'sends waba_id, to and the message object' do
|
|
294
|
+
captured = capture_post('/v1/waba/messages/reply')
|
|
295
|
+
|
|
296
|
+
client.waba_reply(waba_id: 'WABA1', to: '628111', message: { 'type' => 'text', 'text' => 'halo' })
|
|
297
|
+
|
|
298
|
+
expect(captured.body).to eq(
|
|
299
|
+
auth.merge('waba_id' => 'WABA1', 'to' => '628111',
|
|
300
|
+
'message' => { 'type' => 'text', 'text' => 'halo' })
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
describe '#waba_conversations' do
|
|
306
|
+
it 'sends no pagination params when omitted' do
|
|
307
|
+
captured = capture_post('/v1/waba/conversations')
|
|
308
|
+
|
|
309
|
+
client.waba_conversations
|
|
310
|
+
|
|
311
|
+
expect(captured.body).to eq(auth)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it 'sends limit and page when provided' do
|
|
315
|
+
captured = capture_post('/v1/waba/conversations')
|
|
316
|
+
|
|
317
|
+
client.waba_conversations(limit: 25, page: 2)
|
|
318
|
+
|
|
319
|
+
expect(captured.body).to eq(auth.merge('limit' => 25, 'page' => 2))
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
describe '#waba_templates_sync' do
|
|
324
|
+
it 'sends waba_id' do
|
|
325
|
+
captured = capture_post('/v1/waba/templates/sync')
|
|
326
|
+
|
|
327
|
+
client.waba_templates_sync(waba_id: 'WABA1')
|
|
328
|
+
|
|
329
|
+
expect(captured.body).to eq(auth.merge('waba_id' => 'WABA1'))
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
describe '#waba_send_otp' do
|
|
334
|
+
it 'sends waba_id, to and template_name' do
|
|
335
|
+
captured = capture_post('/v1/waba/send-otp')
|
|
336
|
+
|
|
337
|
+
client.waba_send_otp(waba_id: 'WABA1', to: '628111', template_name: 'auth_otp')
|
|
338
|
+
|
|
339
|
+
expect(captured.body).to eq(
|
|
340
|
+
auth.merge('waba_id' => 'WABA1', 'to' => '628111', 'template_name' => 'auth_otp')
|
|
341
|
+
)
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
describe '#waba_verify_otp' do
|
|
346
|
+
it 'sends waba_id, to and otp_code' do
|
|
347
|
+
captured = capture_post('/v1/waba/verify-otp')
|
|
348
|
+
|
|
349
|
+
client.waba_verify_otp(waba_id: 'WABA1', to: '628111', otp_code: '123456')
|
|
350
|
+
|
|
351
|
+
expect(captured.body).to eq(
|
|
352
|
+
auth.merge('waba_id' => 'WABA1', 'to' => '628111', 'otp_code' => '123456')
|
|
353
|
+
)
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# --- Devices ---
|
|
358
|
+
|
|
359
|
+
describe '#create_device' do
|
|
360
|
+
it 'sends package_id only' do
|
|
361
|
+
captured = capture_post('/v1/create-device')
|
|
362
|
+
|
|
363
|
+
client.create_device(package_id: 'PKG1')
|
|
364
|
+
|
|
365
|
+
expect(captured.body).to eq(auth.merge('package_id' => 'PKG1'))
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
it 'sends voucher_code when provided' do
|
|
369
|
+
captured = capture_post('/v1/create-device')
|
|
370
|
+
|
|
371
|
+
client.create_device(package_id: 7, voucher_code: 'PROMO10')
|
|
372
|
+
|
|
373
|
+
expect(captured.body).to eq(auth.merge('package_id' => 7, 'voucher_code' => 'PROMO10'))
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
describe '#connect_device' do
|
|
378
|
+
it 'sends device_id' do
|
|
379
|
+
captured = capture_post('/v1/connect-device')
|
|
380
|
+
|
|
381
|
+
client.connect_device(device_id: 'DEV1')
|
|
382
|
+
|
|
383
|
+
expect(captured.body).to eq(auth.merge('device_id' => 'DEV1'))
|
|
384
|
+
end
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
describe '#renew_device' do
|
|
388
|
+
it 'sends device_id and package_id' do
|
|
389
|
+
captured = capture_post('/v1/renew-device')
|
|
390
|
+
|
|
391
|
+
client.renew_device(device_id: 'DEV1', package_id: 'PKG1')
|
|
392
|
+
|
|
393
|
+
expect(captured.body).to eq(auth.merge('device_id' => 'DEV1', 'package_id' => 'PKG1'))
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
it 'sends voucher_code when provided' do
|
|
397
|
+
captured = capture_post('/v1/renew-device')
|
|
398
|
+
|
|
399
|
+
client.renew_device(device_id: 'DEV1', package_id: 'PKG1', voucher_code: 'VOUCH')
|
|
400
|
+
|
|
401
|
+
expect(captured.body).to eq(
|
|
402
|
+
auth.merge('device_id' => 'DEV1', 'package_id' => 'PKG1', 'voucher_code' => 'VOUCH')
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
describe '#list_devices' do
|
|
408
|
+
it 'sends no pagination params when omitted' do
|
|
409
|
+
captured = capture_post('/v1/list-devices')
|
|
410
|
+
|
|
411
|
+
client.list_devices
|
|
412
|
+
|
|
413
|
+
expect(captured.body).to eq(auth)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
it 'sends page and limit when provided' do
|
|
417
|
+
captured = capture_post('/v1/list-devices')
|
|
418
|
+
|
|
419
|
+
client.list_devices(page: 3, limit: 50)
|
|
420
|
+
|
|
421
|
+
expect(captured.body).to eq(auth.merge('page' => 3, 'limit' => 50))
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
describe '#device_status' do
|
|
426
|
+
it 'sends device_id' do
|
|
427
|
+
captured = capture_post('/v1/device-status')
|
|
428
|
+
|
|
429
|
+
client.device_status(device_id: 'DEV1')
|
|
430
|
+
|
|
431
|
+
expect(captured.body).to eq(auth.merge('device_id' => 'DEV1'))
|
|
432
|
+
end
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
describe '#device_status_enhanced' do
|
|
436
|
+
it 'sends device_id' do
|
|
437
|
+
captured = capture_post('/v1/device-status-enhanced')
|
|
438
|
+
|
|
439
|
+
client.device_status_enhanced(device_id: 'DEV1')
|
|
440
|
+
|
|
441
|
+
expect(captured.body).to eq(auth.merge('device_id' => 'DEV1'))
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# --- Contacts ---
|
|
446
|
+
|
|
447
|
+
describe '#save_contact' do
|
|
448
|
+
it 'sends nama and nomor (never name/phone)' do
|
|
449
|
+
captured = capture_post('/v1/save-contact')
|
|
450
|
+
|
|
451
|
+
client.save_contact(nama: 'Budi', nomor: '628111')
|
|
452
|
+
|
|
453
|
+
expect(captured.body).to eq(auth.merge('nama' => 'Budi', 'nomor' => '628111'))
|
|
454
|
+
expect(captured.body).not_to have_key('name')
|
|
455
|
+
expect(captured.body).not_to have_key('phone')
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it 'sends device_id when provided' do
|
|
459
|
+
captured = capture_post('/v1/save-contact')
|
|
460
|
+
|
|
461
|
+
client.save_contact(nama: 'Budi', nomor: '628111', device_id: 'DEV1')
|
|
462
|
+
|
|
463
|
+
expect(captured.body).to eq(
|
|
464
|
+
auth.merge('nama' => 'Budi', 'nomor' => '628111', 'device_id' => 'DEV1')
|
|
465
|
+
)
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
describe '#save_contacts_bulk' do
|
|
470
|
+
it 'sends contacts as an array of nama/nomor' do
|
|
471
|
+
captured = capture_post('/v1/save-contacts-bulk')
|
|
472
|
+
|
|
473
|
+
client.save_contacts_bulk(
|
|
474
|
+
contacts: [{ nama: 'Budi', nomor: '628111' }, { nama: 'Ani', nomor: '628222' }]
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
expect(captured.body).to eq(
|
|
478
|
+
auth.merge(
|
|
479
|
+
'contacts' => [{ 'nama' => 'Budi', 'nomor' => '628111' },
|
|
480
|
+
{ 'nama' => 'Ani', 'nomor' => '628222' }]
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
expect(captured.body['contacts']).to be_an(Array)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
it 'sends device_id when provided' do
|
|
487
|
+
captured = capture_post('/v1/save-contacts-bulk')
|
|
488
|
+
|
|
489
|
+
client.save_contacts_bulk(contacts: [{ nama: 'Budi', nomor: '628111' }], device_id: 'DEV1')
|
|
490
|
+
|
|
491
|
+
expect(captured.body['device_id']).to eq('DEV1')
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# --- OTP v1 ---
|
|
496
|
+
|
|
497
|
+
describe '#generate_otp' do
|
|
498
|
+
it 'sends snake_case otp_length/otp_type plus camelCase custom text fields' do
|
|
499
|
+
captured = capture_post('/v1/generate-otp')
|
|
500
|
+
|
|
501
|
+
client.generate_otp(
|
|
502
|
+
device_id: 'DEV1',
|
|
503
|
+
phone: '628111',
|
|
504
|
+
otp_length: 6,
|
|
505
|
+
otp_type: 'numeric',
|
|
506
|
+
custom_otp_text: 'Kirimi',
|
|
507
|
+
custom_otp_message: 'Your OTP is {otp}'
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
expect(captured.body).to eq(
|
|
511
|
+
auth.merge(
|
|
512
|
+
'device_id' => 'DEV1',
|
|
513
|
+
'phone' => '628111',
|
|
514
|
+
'otp_length' => 6,
|
|
515
|
+
'otp_type' => 'numeric',
|
|
516
|
+
'customOtpText' => 'Kirimi',
|
|
517
|
+
'customOtpMessage' => 'Your OTP is {otp}'
|
|
518
|
+
)
|
|
519
|
+
)
|
|
520
|
+
expect(captured.body).not_to have_key('otpLength')
|
|
521
|
+
expect(captured.body).not_to have_key('otpType')
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
it 'includes typing effect params when provided' do
|
|
525
|
+
captured = capture_post('/v1/generate-otp')
|
|
526
|
+
|
|
527
|
+
client.generate_otp(device_id: 'DEV1', phone: '628111',
|
|
528
|
+
enable_typing_effect: true, typing_speed_ms: 300)
|
|
529
|
+
|
|
530
|
+
expect(captured.body).to include('enableTypingEffect' => true, 'typingSpeedMs' => 300)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
it 'omits optional fields when nil' do
|
|
534
|
+
captured = capture_post('/v1/generate-otp')
|
|
535
|
+
|
|
536
|
+
client.generate_otp(device_id: 'DEV1', phone: '628111')
|
|
537
|
+
|
|
538
|
+
expect(captured.body).to eq(auth.merge('device_id' => 'DEV1', 'phone' => '628111'))
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
describe '#validate_otp' do
|
|
543
|
+
it 'sends device_id, phone and otp' do
|
|
544
|
+
captured = capture_post('/v1/validate-otp')
|
|
545
|
+
|
|
546
|
+
client.validate_otp(device_id: 'DEV1', phone: '628111', otp: '123456')
|
|
547
|
+
|
|
548
|
+
expect(captured.body).to eq(
|
|
549
|
+
auth.merge('device_id' => 'DEV1', 'phone' => '628111', 'otp' => '123456')
|
|
550
|
+
)
|
|
551
|
+
end
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# --- OTP v2 ---
|
|
555
|
+
|
|
556
|
+
describe '#send_otp_v2' do
|
|
557
|
+
it 'supports method whatsapp with app_name only' do
|
|
558
|
+
captured = capture_post('/v2/otp/send')
|
|
559
|
+
|
|
560
|
+
client.send_otp_v2(phone: '628111', method: 'whatsapp', app_name: 'MyApp')
|
|
561
|
+
|
|
562
|
+
expect(captured.body).to eq(
|
|
563
|
+
auth.merge('phone' => '628111', 'method' => 'whatsapp', 'app_name' => 'MyApp')
|
|
564
|
+
)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
it 'supports method device with device_id and custom_message' do
|
|
568
|
+
captured = capture_post('/v2/otp/send')
|
|
569
|
+
|
|
570
|
+
client.send_otp_v2(
|
|
571
|
+
phone: '628111',
|
|
572
|
+
method: 'device',
|
|
573
|
+
device_id: 'DEV1',
|
|
574
|
+
custom_message: 'Your OTP is {{otp}}'
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
expect(captured.body).to eq(
|
|
578
|
+
auth.merge('phone' => '628111', 'method' => 'device', 'device_id' => 'DEV1',
|
|
579
|
+
'custom_message' => 'Your OTP is {{otp}}')
|
|
580
|
+
)
|
|
581
|
+
expect(captured.body).not_to have_key('waba_id')
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
it 'supports method waba_user with waba_id and template_name' do
|
|
585
|
+
captured = capture_post('/v2/otp/send')
|
|
586
|
+
|
|
587
|
+
client.send_otp_v2(
|
|
588
|
+
phone: '628111',
|
|
589
|
+
method: 'waba_user',
|
|
590
|
+
waba_id: 'WABA1',
|
|
591
|
+
template_name: 'auth_otp'
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
expect(captured.body).to eq(
|
|
595
|
+
auth.merge('phone' => '628111', 'method' => 'waba_user', 'waba_id' => 'WABA1',
|
|
596
|
+
'template_name' => 'auth_otp')
|
|
597
|
+
)
|
|
598
|
+
expect(captured.body).not_to have_key('device_id')
|
|
599
|
+
expect(captured.body).not_to have_key('template_code')
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
it 'sends only phone when method is omitted' do
|
|
603
|
+
captured = capture_post('/v2/otp/send')
|
|
604
|
+
|
|
605
|
+
client.send_otp_v2(phone: '628111')
|
|
606
|
+
|
|
607
|
+
expect(captured.body).to eq(auth.merge('phone' => '628111'))
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
describe '#verify_otp_v2' do
|
|
612
|
+
it 'sends phone and otp_code' do
|
|
613
|
+
captured = capture_post('/v2/otp/verify')
|
|
614
|
+
|
|
615
|
+
client.verify_otp_v2(phone: '628111', otp_code: '123456')
|
|
616
|
+
|
|
617
|
+
expect(captured.body).to eq(auth.merge('phone' => '628111', 'otp_code' => '123456'))
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# --- OTP Reverse ---
|
|
622
|
+
|
|
623
|
+
describe '#otp_reverse_create' do
|
|
624
|
+
it 'sends required phone and device_id' do
|
|
625
|
+
captured = capture_post('/v2/otp-reverse/create')
|
|
626
|
+
|
|
627
|
+
client.otp_reverse_create(phone: '628111', device_id: 'DEV1')
|
|
628
|
+
|
|
629
|
+
expect(captured.body).to eq(auth.merge('phone' => '628111', 'device_id' => 'DEV1'))
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
it 'sends every optional field when provided' do
|
|
633
|
+
captured = capture_post('/v2/otp-reverse/create')
|
|
634
|
+
|
|
635
|
+
client.otp_reverse_create(
|
|
636
|
+
phone: '628111',
|
|
637
|
+
device_id: 'DEV1',
|
|
638
|
+
app_name: 'MyApp',
|
|
639
|
+
callback_url: 'https://cb.test/hook',
|
|
640
|
+
custom_message: 'Kirim {{token}} dari {{phone}}',
|
|
641
|
+
success_message: 'Berhasil',
|
|
642
|
+
failure_message: 'Gagal'
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
expect(captured.body).to eq(
|
|
646
|
+
auth.merge(
|
|
647
|
+
'phone' => '628111',
|
|
648
|
+
'device_id' => 'DEV1',
|
|
649
|
+
'app_name' => 'MyApp',
|
|
650
|
+
'callback_url' => 'https://cb.test/hook',
|
|
651
|
+
'custom_message' => 'Kirim {{token}} dari {{phone}}',
|
|
652
|
+
'success_message' => 'Berhasil',
|
|
653
|
+
'failure_message' => 'Gagal'
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
describe '#otp_reverse_status' do
|
|
660
|
+
it 'sends token' do
|
|
661
|
+
captured = capture_post('/v2/otp-reverse/status')
|
|
662
|
+
|
|
663
|
+
client.otp_reverse_status(token: '01HZZTOKEN')
|
|
664
|
+
|
|
665
|
+
expect(captured.body).to eq(auth.merge('token' => '01HZZTOKEN'))
|
|
666
|
+
end
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# --- Packages & Deposits ---
|
|
670
|
+
|
|
671
|
+
describe '#list_packages' do
|
|
672
|
+
it 'sends only auth' do
|
|
673
|
+
captured = capture_post('/v1/list-packages')
|
|
674
|
+
|
|
675
|
+
resp = client.list_packages
|
|
676
|
+
|
|
677
|
+
expect(captured.body).to eq(auth)
|
|
678
|
+
expect(resp.success?).to be true
|
|
679
|
+
end
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
describe '#create_deposit' do
|
|
683
|
+
it 'sends nominal' do
|
|
684
|
+
captured = capture_post('/v1/create-deposit')
|
|
685
|
+
|
|
686
|
+
client.create_deposit(nominal: 50_000)
|
|
687
|
+
|
|
688
|
+
expect(captured.body).to eq(auth.merge('nominal' => 50_000))
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
describe '#deposit_status' do
|
|
693
|
+
it 'sends ref' do
|
|
694
|
+
captured = capture_post('/v1/deposit-status')
|
|
695
|
+
|
|
696
|
+
client.deposit_status(ref: 'DEP123')
|
|
697
|
+
|
|
698
|
+
expect(captured.body).to eq(auth.merge('ref' => 'DEP123'))
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
describe '#cancel_deposit' do
|
|
703
|
+
it 'sends ref' do
|
|
704
|
+
captured = capture_post('/v1/cancel-deposit')
|
|
705
|
+
|
|
706
|
+
client.cancel_deposit(ref: 'DEP123')
|
|
707
|
+
|
|
708
|
+
expect(captured.body).to eq(auth.merge('ref' => 'DEP123'))
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
describe '#list_deposits' do
|
|
713
|
+
it 'sends no params when omitted' do
|
|
714
|
+
captured = capture_post('/v1/list-deposits')
|
|
715
|
+
|
|
716
|
+
client.list_deposits
|
|
717
|
+
|
|
718
|
+
expect(captured.body).to eq(auth)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
it 'sends page, limit and status when provided' do
|
|
722
|
+
captured = capture_post('/v1/list-deposits')
|
|
723
|
+
|
|
724
|
+
client.list_deposits(page: 1, limit: 10, status: 'unpaid')
|
|
725
|
+
|
|
726
|
+
expect(captured.body).to eq(auth.merge('page' => 1, 'limit' => 10, 'status' => 'unpaid'))
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
# --- User ---
|
|
731
|
+
|
|
732
|
+
describe '#user_info' do
|
|
733
|
+
it 'sends only auth and wraps the response' do
|
|
734
|
+
captured = capture_post(
|
|
735
|
+
'/v1/user-info',
|
|
736
|
+
response_body: {
|
|
737
|
+
'success' => true,
|
|
738
|
+
'data' => { 'name' => 'John Doe', 'email' => 'john@example.com' },
|
|
739
|
+
'message' => 'OK'
|
|
740
|
+
}
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
resp = client.user_info
|
|
744
|
+
|
|
745
|
+
expect(captured.body).to eq(auth)
|
|
746
|
+
expect(resp).to be_a(Kirimi::Response)
|
|
747
|
+
expect(resp.success).to be true
|
|
748
|
+
expect(resp.success?).to be true
|
|
749
|
+
expect(resp.data['name']).to eq('John Doe')
|
|
750
|
+
expect(resp.raw).to be_a(Hash)
|
|
751
|
+
expect(resp.raw['success']).to be true
|
|
752
|
+
end
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# --- Error handling ---
|
|
756
|
+
|
|
757
|
+
describe 'error handling' do
|
|
758
|
+
it 'raises ApiError with status_code 401 on 401' do
|
|
759
|
+
stub_post_exact(
|
|
760
|
+
'/v1/user-info',
|
|
761
|
+
auth,
|
|
762
|
+
response_body: { 'success' => false, 'message' => 'Unauthorized' },
|
|
763
|
+
status: 401
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
expect { client.user_info }.to raise_error(Kirimi::ApiError) do |err|
|
|
767
|
+
expect(err.status_code).to eq(401)
|
|
768
|
+
expect(err.message).to eq('Unauthorized')
|
|
769
|
+
expect(err).to be_a(Kirimi::Error)
|
|
770
|
+
end
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
it 'raises ApiError with status_code 402 when balance is insufficient' do
|
|
774
|
+
stub_post_exact(
|
|
775
|
+
'/v2/otp/send',
|
|
776
|
+
auth.merge('phone' => '628111', 'method' => 'whatsapp'),
|
|
777
|
+
response_body: { 'success' => false, 'message' => 'Insufficient balance' },
|
|
778
|
+
status: 402
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
expect { client.send_otp_v2(phone: '628111', method: 'whatsapp') }
|
|
782
|
+
.to raise_error(Kirimi::ApiError) { |err| expect(err.status_code).to eq(402) }
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
it 'raises ApiError with status_code 500 on server error' do
|
|
786
|
+
stub_post_exact(
|
|
787
|
+
'/v1/list-devices',
|
|
788
|
+
auth,
|
|
789
|
+
response_body: { 'success' => false, 'message' => 'Internal Server Error' },
|
|
790
|
+
status: 500
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
expect { client.list_devices }.to raise_error(Kirimi::ApiError) do |err|
|
|
794
|
+
expect(err.status_code).to eq(500)
|
|
795
|
+
end
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
it 'exposes the parsed body on the error' do
|
|
799
|
+
stub_post_exact(
|
|
800
|
+
'/v1/user-info',
|
|
801
|
+
auth,
|
|
802
|
+
response_body: { 'success' => false, 'message' => 'Unauthorized' },
|
|
803
|
+
status: 401
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
expect { client.user_info }.to raise_error(Kirimi::ApiError) do |err|
|
|
807
|
+
expect(err.response_data).to eq('success' => false, 'message' => 'Unauthorized')
|
|
808
|
+
end
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
it 'raises NetworkError when the connection fails' do
|
|
812
|
+
stub_request(:post, "#{base_url}/v1/user-info").to_raise(SocketError.new('getaddrinfo failed'))
|
|
813
|
+
|
|
814
|
+
expect { client.user_info }.to raise_error(Kirimi::NetworkError, /Network error/)
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kirimi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kirimi
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|
|
@@ -24,6 +24,34 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '13.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rspec
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.12'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.12'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: webmock
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '3.19'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '3.19'
|
|
27
55
|
description: Send WhatsApp messages, OTP, broadcasts, and more via the Kirimi API.
|
|
28
56
|
email:
|
|
29
57
|
- support@kirimi.id
|
|
@@ -40,7 +68,7 @@ files:
|
|
|
40
68
|
- lib/kirimi/errors.rb
|
|
41
69
|
- lib/kirimi/response.rb
|
|
42
70
|
- lib/kirimi/version.rb
|
|
43
|
-
-
|
|
71
|
+
- spec/client_spec.rb
|
|
44
72
|
homepage: https://github.com/kiriminow/kirimi-ruby
|
|
45
73
|
licenses:
|
|
46
74
|
- MIT
|
|
@@ -48,7 +76,7 @@ metadata:
|
|
|
48
76
|
homepage_uri: https://github.com/kiriminow/kirimi-ruby
|
|
49
77
|
source_code_uri: https://github.com/kiriminow/kirimi-ruby
|
|
50
78
|
changelog_uri: https://github.com/kiriminow/kirimi-ruby/blob/main/CHANGELOG.md
|
|
51
|
-
post_install_message:
|
|
79
|
+
post_install_message:
|
|
52
80
|
rdoc_options: []
|
|
53
81
|
require_paths:
|
|
54
82
|
- lib
|
|
@@ -63,9 +91,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
63
91
|
- !ruby/object:Gem::Version
|
|
64
92
|
version: '0'
|
|
65
93
|
requirements: []
|
|
66
|
-
rubygems_version: 3.
|
|
67
|
-
signing_key:
|
|
94
|
+
rubygems_version: 3.5.22
|
|
95
|
+
signing_key:
|
|
68
96
|
specification_version: 4
|
|
69
97
|
summary: Official Ruby SDK for Kirimi WhatsApp API
|
|
70
98
|
test_files:
|
|
71
|
-
-
|
|
99
|
+
- spec/client_spec.rb
|
data/test/client_test.rb
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'minitest/autorun'
|
|
4
|
-
require 'minitest/pride'
|
|
5
|
-
require 'json'
|
|
6
|
-
require 'ostruct'
|
|
7
|
-
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
9
|
-
require 'kirimi'
|
|
10
|
-
|
|
11
|
-
# Minimal HTTP response stub
|
|
12
|
-
FakeResponse = Struct.new(:code, :body, :message) do
|
|
13
|
-
def [](key); nil; end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Captures the last request body sent via Net::HTTP
|
|
17
|
-
module NetHTTPStub
|
|
18
|
-
@stub_status = 200
|
|
19
|
-
@stub_body = {}
|
|
20
|
-
@last_body = nil
|
|
21
|
-
@last_path = nil
|
|
22
|
-
|
|
23
|
-
class << self
|
|
24
|
-
attr_accessor :stub_status, :stub_body, :last_body, :last_path
|
|
25
|
-
|
|
26
|
-
def setup(status:, body:)
|
|
27
|
-
@stub_status = status
|
|
28
|
-
@stub_body = body
|
|
29
|
-
@last_body = nil
|
|
30
|
-
@last_path = nil
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Monkey-patch Net::HTTP for tests only
|
|
36
|
-
module Net
|
|
37
|
-
class HTTP
|
|
38
|
-
def request(req)
|
|
39
|
-
NetHTTPStub.last_body = JSON.parse(req.body) rescue req.body
|
|
40
|
-
NetHTTPStub.last_path = req.path
|
|
41
|
-
FakeResponse.new(
|
|
42
|
-
NetHTTPStub.stub_status.to_s,
|
|
43
|
-
NetHTTPStub.stub_body.to_json,
|
|
44
|
-
'OK'
|
|
45
|
-
)
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
class ClientTest < Minitest::Test
|
|
51
|
-
def setup
|
|
52
|
-
@client = Kirimi::Client.new(user_code: 'USER', secret: 'SECRET')
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def stub(status: 200, body: { 'success' => true, 'data' => nil, 'message' => 'ok' })
|
|
56
|
-
NetHTTPStub.setup(status: status, body: body)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# ---- send_message ----
|
|
60
|
-
|
|
61
|
-
def test_send_message_correct_body
|
|
62
|
-
stub(body: { 'success' => true, 'data' => { 'id' => 'msg_1' }, 'message' => 'sent' })
|
|
63
|
-
|
|
64
|
-
resp = @client.send_message(device_id: 'DEV1', phone: '628111', message: 'Hello!')
|
|
65
|
-
|
|
66
|
-
assert_equal 'USER', NetHTTPStub.last_body['user_code']
|
|
67
|
-
assert_equal 'SECRET', NetHTTPStub.last_body['secret']
|
|
68
|
-
assert_equal 'DEV1', NetHTTPStub.last_body['device_id']
|
|
69
|
-
assert_equal '628111', NetHTTPStub.last_body['phone']
|
|
70
|
-
assert_equal 'Hello!', NetHTTPStub.last_body['message']
|
|
71
|
-
assert_instance_of Kirimi::Response, resp
|
|
72
|
-
assert resp.success?
|
|
73
|
-
assert_equal 'msg_1', resp.data['id']
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def test_send_message_includes_media_url
|
|
77
|
-
stub
|
|
78
|
-
@client.send_message(device_id: 'DEV1', phone: '628111', message: 'pic', media_url: 'https://img.example.com/a.jpg')
|
|
79
|
-
assert_equal 'https://img.example.com/a.jpg', NetHTTPStub.last_body['media_url']
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def test_send_message_omits_media_url_when_nil
|
|
83
|
-
stub
|
|
84
|
-
@client.send_message(device_id: 'DEV1', phone: '628111', message: 'hi')
|
|
85
|
-
refute NetHTTPStub.last_body.key?('media_url')
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# ---- generate_otp ----
|
|
89
|
-
|
|
90
|
-
def test_generate_otp_with_optional_params
|
|
91
|
-
stub(body: { 'success' => true, 'data' => { 'otp' => '123456' }, 'message' => 'OTP sent' })
|
|
92
|
-
|
|
93
|
-
resp = @client.generate_otp(
|
|
94
|
-
device_id: 'DEV1',
|
|
95
|
-
phone: '628111',
|
|
96
|
-
otp_length: 6,
|
|
97
|
-
otp_type: 'numeric',
|
|
98
|
-
custom_otp_message: 'Your code is {otp}'
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
assert_equal 6, NetHTTPStub.last_body['otp_length']
|
|
102
|
-
assert_equal 'numeric', NetHTTPStub.last_body['otp_type']
|
|
103
|
-
assert_equal 'Your code is {otp}', NetHTTPStub.last_body['customOtpMessage']
|
|
104
|
-
assert resp.success?
|
|
105
|
-
assert_equal '123456', resp.data['otp']
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def test_generate_otp_omits_optional_when_nil
|
|
109
|
-
stub
|
|
110
|
-
@client.generate_otp(device_id: 'DEV1', phone: '628111')
|
|
111
|
-
refute NetHTTPStub.last_body.key?('otp_length')
|
|
112
|
-
refute NetHTTPStub.last_body.key?('customOtpMessage')
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# ---- error handling ----
|
|
116
|
-
|
|
117
|
-
def test_raises_api_error_on_401
|
|
118
|
-
stub(status: 401, body: { 'success' => false, 'message' => 'Unauthorized' })
|
|
119
|
-
|
|
120
|
-
err = assert_raises(Kirimi::ApiError) { @client.user_info }
|
|
121
|
-
assert_equal 401, err.status_code
|
|
122
|
-
assert_equal 'Unauthorized', err.message
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def test_raises_api_error_on_500
|
|
126
|
-
stub(status: 500, body: { 'success' => false, 'message' => 'Server Error' })
|
|
127
|
-
|
|
128
|
-
err = assert_raises(Kirimi::ApiError) { @client.list_devices }
|
|
129
|
-
assert_equal 500, err.status_code
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# ---- response wrapping ----
|
|
133
|
-
|
|
134
|
-
def test_response_wraps_data_correctly
|
|
135
|
-
stub(body: { 'success' => true, 'data' => { 'name' => 'John' }, 'message' => 'OK' })
|
|
136
|
-
|
|
137
|
-
resp = @client.user_info
|
|
138
|
-
|
|
139
|
-
assert_instance_of Kirimi::Response, resp
|
|
140
|
-
assert resp.success?
|
|
141
|
-
assert_equal true, resp.success
|
|
142
|
-
assert_equal 'John', resp.data['name']
|
|
143
|
-
assert_equal 'OK', resp.message
|
|
144
|
-
assert_instance_of Hash, resp.raw
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
# ---- broadcast phones array ----
|
|
148
|
-
|
|
149
|
-
def test_broadcast_joins_phones_array
|
|
150
|
-
stub
|
|
151
|
-
@client.broadcast_message(device_id: 'DEV1', phones: %w[628111 628222 628333], message: 'Promo!')
|
|
152
|
-
assert_equal '628111,628222,628333', NetHTTPStub.last_body['phones']
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def test_broadcast_accepts_phones_string
|
|
156
|
-
stub
|
|
157
|
-
@client.broadcast_message(device_id: 'DEV1', phones: '628111,628222', message: 'Hi')
|
|
158
|
-
assert_equal '628111,628222', NetHTTPStub.last_body['phones']
|
|
159
|
-
end
|
|
160
|
-
end
|