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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0bc013f5f011e16f83060dc902e32b6cf6c45312c2d6c443cb882503babed732
4
- data.tar.gz: '082c9dcd26bee5df651c865dc77c93222c49f8fa70e68d4e5970ef9013efad17'
3
+ metadata.gz: 6f1ffed06942da09afddf27319d189d03aa8230e6f711b7cd8b5e7fabc553507
4
+ data.tar.gz: 477551b4134d8fda91be2ac6e7428ec76ccc19776135f8c44d653db3c9ec40c2
5
5
  SHA512:
6
- metadata.gz: 487b38bee90b1463c0550efdb36a7646e0bdfd50ec2f0e598407bf4aacf3b0a3a770f0c0fead719b0fb98ad68debfa54e8b26574a538c5b76b4ff1cefa94d0a4
7
- data.tar.gz: cc5f8545838710df3641cf0207aa2dc9d168c906f76a2edc1d288942f4ce05677ed9ffa50e7c2c45c5b6128dcafec5950cf0198bc821cf989fcd66cf5d7fafe2
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('test/**/*_test.rb')
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:, phone:, message:, media_url: nil)
22
- body = { device_id: device_id, phone: phone, message: message }
23
- body[:media_url] = media_url if media_url
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:, phone:, file:, file_name:, message: nil)
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
- 'phone' => phone,
34
- 'fileName' => file_name
40
+ 'receiver' => receiver
35
41
  }
36
- fields['message'] = message if message
37
- post_multipart('/v1/send-message-file', fields, file_io, file_name)
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:, phone:, message:, media_url: nil)
43
- body = { device_id: device_id, phone: phone, message: message }
44
- body[:media_url] = media_url if media_url
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
- # --- WABA ---
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 send_waba_message(device_id:, phone:, message:)
51
- post('/v1/waba/send-message', { device_id: device_id, phone: phone, message: message })
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 list_devices
57
- post('/v1/list-devices', {})
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(phone:, name: nil, email: nil)
77
- body = { phone: phone }
78
- body[:name] = name if name
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
- # --- OTP ---
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, custom_otp_message: 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] = otp_length if otp_length
88
- body[:otp_type] = otp_type if otp_type
89
- body[:customOtpMessage] = custom_otp_message if custom_otp_message
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:, device_id:, method: nil, app_name: nil, template_code: nil, custom_message: nil)
100
- body = { phone: phone, device_id: device_id }
101
- body[:method] = method if method
102
- body[:app_name] = app_name if app_name
103
- body[:template_code] = template_code if template_code
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
- # --- Broadcast ---
204
+ # --- OTP Reverse ---
113
205
 
114
- def broadcast_message(device_id:, phones:, message:, delay: nil)
115
- phones_str = phones.is_a?(Array) ? phones.join(',') : phones
116
- body = { device_id: device_id, phones: phones_str, message: message }
117
- body[:delay] = delay if delay
118
- post('/v1/broadcast-message', body)
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
- # --- Deposits ---
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'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kirimi
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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.1.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-04-14 00:00:00.000000000 Z
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
- - test/client_test.rb
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.0.3.1
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
- - test/client_test.rb
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