max_bot 0.1.1 → 0.2.1

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.
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'faraday'
5
+
6
+ module Max
7
+ module Bot
8
+ class RecordingHttp
9
+ attr_reader :posts, :gets, :deletes
10
+
11
+ def initialize
12
+ @posts = []
13
+ @gets = []
14
+ @deletes = []
15
+ end
16
+
17
+ def post(path, query: nil, body: nil)
18
+ @posts << { path: path, query: query, body: body }
19
+ { ok: true }
20
+ end
21
+
22
+ def get(path, query: {})
23
+ @gets << { path: path, query: query }
24
+ { updates: [] }
25
+ end
26
+
27
+ def delete(path, query: nil)
28
+ @deletes << { path: path, query: query }
29
+ { success: true }
30
+ end
31
+ end
32
+
33
+ class ApiSendTest < Minitest::Test
34
+ def test_send_message_merges_attachment_and_attachments
35
+ rec = RecordingHttp.new
36
+ api = Api.new('token', http: rec)
37
+
38
+ api.send_message(
39
+ 'Hello',
40
+ chat_id: 10,
41
+ attachment: Attachments.sticker(code: 'smile'),
42
+ attachments: [Attachments.share(url: 'https://a.example')]
43
+ )
44
+
45
+ atts = rec.posts.first[:body][:attachments]
46
+ assert_equal 2, atts.size
47
+ assert_equal 'sticker', atts[0][:type]
48
+ assert_equal 'share', atts[1][:type]
49
+ assert_equal 10, rec.posts.first[:query][:chat_id]
50
+ end
51
+
52
+ def test_get_updates_forwards_query
53
+ rec = RecordingHttp.new
54
+ api = Api.new('tok', http: rec)
55
+ api.get_updates(marker: 7, limit: 50, timeout: 15, types: %w[message_created])
56
+
57
+ q = rec.gets.first[:query]
58
+ assert_equal 7, q[:marker]
59
+ assert_equal 50, q[:limit]
60
+ assert_equal 15, q[:timeout]
61
+ assert_equal 'message_created', q[:types]
62
+ end
63
+
64
+ def test_set_webhook_json_body
65
+ rec = RecordingHttp.new
66
+ api = Api.new('tok', http: rec)
67
+ api.set_webhook(url: 'https://hook', update_types: %w[a], secret: 'sec')
68
+
69
+ body = rec.posts.first[:body]
70
+ assert_equal 'https://hook', body[:url]
71
+ assert_equal %w[a], body[:update_types]
72
+ assert_equal 'sec', body[:secret]
73
+ end
74
+
75
+ def test_delete_webhook_query
76
+ rec = RecordingHttp.new
77
+ api = Api.new('tok', http: rec)
78
+ api.delete_webhook('https://hook')
79
+
80
+ assert_equal({ url: 'https://hook' }, rec.deletes.first[:query])
81
+ end
82
+
83
+ def test_chats_query
84
+ rec = RecordingHttp.new
85
+ api = Api.new('tok', http: rec)
86
+ api.chats(count: 25, marker: 3)
87
+
88
+ assert_equal({ count: 25, marker: 3 }, rec.gets.first[:query])
89
+ end
90
+
91
+ def test_create_upload_posts_type
92
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
93
+ stub.post('/uploads') do |env|
94
+ assert_equal 'image', env.params['type']
95
+ [200, { 'Content-Type' => 'application/json' }, '{"url":"https://cdn.example/up"}']
96
+ end
97
+ end
98
+
99
+ conn = Faraday.new do |f|
100
+ f.adapter :test, stubs
101
+ end
102
+
103
+ http = Http.new('tok', 'https://platform-api.max.ru', connection: conn)
104
+ api = Api.new('tok', http: http)
105
+
106
+ result = api.create_upload(type: :image)
107
+ assert_equal 'https://cdn.example/up', result[:url]
108
+ stubs.verify_stubbed_calls
109
+ end
110
+
111
+ def test_create_upload_rejects_unknown_type
112
+ api = Api.new('tok', http: RecordingHttp.new)
113
+ assert_raises(ArgumentError) { api.create_upload(type: :photo) }
114
+ end
115
+
116
+ def test_upload_file_raises_when_slot_has_no_url
117
+ api = Api.new('tok', http: RecordingHttp.new)
118
+
119
+ def api.create_upload(*)
120
+ { token: 'only' }
121
+ end
122
+
123
+ err = assert_raises(ApiError) { api.upload_file(type: :image, path: __FILE__) }
124
+ assert_match(/url/i, err.message)
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class ApiTest < Minitest::Test
8
+ def test_rejects_blank_token
9
+ assert_raises(ArgumentError) { Api.new(nil) }
10
+ assert_raises(ArgumentError) { Api.new(' ') }
11
+ end
12
+
13
+ def test_send_message_requires_recipient
14
+ api = Api.new('token')
15
+ err = assert_raises(ArgumentError) { api.send_message('hi') }
16
+ assert_match(/chat_id|user_id/, err.message)
17
+ end
18
+
19
+ def test_send_message_requires_payload
20
+ api = Api.new('token')
21
+ err = assert_raises(ArgumentError) { api.send_message(nil, chat_id: 1) }
22
+ assert_match(/text|attachments|attachment|link/, err.message)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class AttachmentsTest < Minitest::Test
8
+ def test_image_with_token
9
+ att = Attachments.image(token: 'abc')
10
+ assert_equal 'image', att[:type]
11
+ assert_equal({ token: 'abc' }, att[:payload])
12
+ end
13
+
14
+ def test_inline_keyboard_structure
15
+ rows = [
16
+ [
17
+ Attachments.callback_button('OK', 'payload-1', intent: 'positive'),
18
+ Attachments.link_button('Site', 'https://example.com')
19
+ ]
20
+ ]
21
+ att = Attachments.inline_keyboard(rows)
22
+ assert_equal 'inline_keyboard', att[:type]
23
+ assert_equal rows, att[:payload][:buttons]
24
+ end
25
+
26
+ def test_location_top_level_coordinates
27
+ att = Attachments.location(latitude: 55.75, longitude: 37.61)
28
+ assert_equal 'location', att[:type]
29
+ assert_in_delta 55.75, att[:latitude]
30
+ assert_in_delta 37.61, att[:longitude]
31
+ end
32
+
33
+ def test_share_payload
34
+ att = Attachments.share(url: 'https://example.com')
35
+ assert_equal 'share', att[:type]
36
+ assert_equal 'https://example.com', att[:payload][:url]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class ClientTest < Minitest::Test
8
+ class DummyApi
9
+ attr_reader :get_updates_calls
10
+
11
+ def initialize(responses)
12
+ @responses = responses.dup
13
+ @get_updates_calls = []
14
+ end
15
+
16
+ def get_updates(**kwargs)
17
+ @get_updates_calls << kwargs
18
+ @responses.shift || { updates: [], marker: nil }
19
+ end
20
+ end
21
+
22
+ def test_initialize_accepts_injected_api
23
+ dummy = DummyApi.new([])
24
+ client = Client.new('token', api: dummy)
25
+ assert_same dummy, client.api
26
+ end
27
+
28
+ def test_run_requires_block
29
+ client = Client.new('token', api: DummyApi.new([]))
30
+ err = assert_raises(ArgumentError) { client.run }
31
+ assert_match(/block/, err.message)
32
+ end
33
+
34
+ def test_class_run_requires_block
35
+ assert_raises(ArgumentError) { Client.run('token') }
36
+ end
37
+
38
+ def test_run_yields_updates_and_passes_marker_on_next_poll
39
+ responses = [
40
+ { updates: [{ id: 1 }], marker: 10 },
41
+ { updates: [{ id: 2 }], marker: 20 }
42
+ ]
43
+ dummy = DummyApi.new(responses)
44
+ seen = []
45
+ client = Client.new('token', api: dummy, sleep: ->(_) {})
46
+
47
+ client.run do |u|
48
+ seen << u
49
+ raise StopIteration if seen.size >= 2
50
+ end
51
+
52
+ assert_equal 2, seen.size
53
+ assert_equal({ id: 1 }, seen[0])
54
+ assert_equal({ id: 2 }, seen[1])
55
+ assert_nil dummy.get_updates_calls[0][:marker]
56
+ assert_equal 10, dummy.get_updates_calls[1][:marker]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class ErrorsTest < Minitest::Test
8
+ def test_api_error_to_s_includes_message_field
9
+ err = ApiError.new('HTTP 400', status: 400, body: { message: 'bad request' })
10
+ assert_match(/HTTP 400/, err.to_s)
11
+ assert_match(/bad request/, err.to_s)
12
+ end
13
+
14
+ def test_api_error_attributes
15
+ err = ApiError.new('x', status: 404, body: { code: 1 })
16
+ assert_equal 404, err.status
17
+ assert_equal({ code: 1 }, err.body)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+ require 'faraday'
5
+
6
+ module Max
7
+ module Bot
8
+ class HttpTest < Minitest::Test
9
+ def test_get_parses_json
10
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
11
+ stub.get('/updates') { [200, { 'Content-Type' => 'application/json' }, '{"updates":[],"marker":null}'] }
12
+ end
13
+
14
+ conn = Faraday.new do |f|
15
+ f.adapter :test, stubs
16
+ end
17
+
18
+ http = Http.new('secret-token', 'https://platform-api.max.ru', connection: conn)
19
+ result = http.get('/updates', query: {})
20
+
21
+ assert_equal [], result[:updates]
22
+ assert_nil result[:marker]
23
+ stubs.verify_stubbed_calls
24
+ end
25
+
26
+ def test_post_sends_json_body_and_auth
27
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
28
+ stub.post('/messages', '{"text":"hi"}') do |env|
29
+ assert_equal 'secret-token', env.request_headers['Authorization']
30
+ assert_match %r{application/json}, env.request_headers['Content-Type']
31
+ [200, { 'Content-Type' => 'application/json' }, '{"message":{"body":{}}}']
32
+ end
33
+ end
34
+
35
+ conn = Faraday.new do |f|
36
+ f.adapter :test, stubs
37
+ end
38
+
39
+ http = Http.new('secret-token', 'https://platform-api.max.ru', connection: conn)
40
+ result = http.post('/messages', query: { chat_id: 1 }, body: { text: 'hi' })
41
+
42
+ assert result.key?(:message)
43
+ stubs.verify_stubbed_calls
44
+ end
45
+
46
+ def test_raises_api_error_on_http_error
47
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
48
+ stub.get('/updates') { [401, {}, '{"message":"unauthorized"}'] }
49
+ end
50
+
51
+ conn = Faraday.new do |f|
52
+ f.adapter :test, stubs
53
+ end
54
+
55
+ http = Http.new('bad', 'https://platform-api.max.ru', connection: conn)
56
+
57
+ err = assert_raises(ApiError) { http.get('/updates') }
58
+ assert_equal 401, err.status
59
+ assert_equal 'unauthorized', err.body[:message]
60
+ end
61
+
62
+ def test_delete_sends_query_and_auth
63
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
64
+ stub.delete('/subscriptions') do |env|
65
+ assert_equal 'tok', env.request_headers['Authorization']
66
+ assert_equal 'https://h', env.params['url']
67
+ [200, { 'Content-Type' => 'application/json' }, '{"success":true}']
68
+ end
69
+ end
70
+
71
+ conn = Faraday.new do |f|
72
+ f.adapter :test, stubs
73
+ end
74
+
75
+ http = Http.new('tok', 'https://platform-api.max.ru', connection: conn)
76
+ result = http.delete('/subscriptions', query: { url: 'https://h' })
77
+ assert_equal true, result[:success]
78
+ stubs.verify_stubbed_calls
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class JsonTest < Minitest::Test
8
+ FakeResponse = Struct.new(:status, :body) do
9
+ def success?
10
+ status >= 200 && status < 300
11
+ end
12
+ end
13
+
14
+ def test_deep_symbolize_nested
15
+ input = { 'a' => 1, 'b' => [{ 'c' => 'd' }] }
16
+ out = Json.deep_symbolize(input)
17
+
18
+ assert_equal({ a: 1, b: [{ c: 'd' }] }, out)
19
+ end
20
+
21
+ def test_decode_response_success
22
+ r = FakeResponse.new(200, '{"a":1}')
23
+ assert_equal({ a: 1 }, Json.decode_response(r))
24
+ end
25
+
26
+ def test_decode_response_empty_body
27
+ r = FakeResponse.new(200, '')
28
+ assert_equal({}, Json.decode_response(r))
29
+ end
30
+
31
+ def test_decode_response_error_raises_api_error
32
+ r = FakeResponse.new(422, '{"message":"nope"}')
33
+ err = assert_raises(ApiError) { Json.decode_response(r) }
34
+ assert_equal 422, err.status
35
+ assert_equal 'nope', err.body[:message]
36
+ end
37
+
38
+ def test_decode_response_invalid_json
39
+ r = FakeResponse.new(200, '{')
40
+ assert_raises(ApiError) { Json.decode_response(r) }
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class UpdateHelpersTest < Minitest::Test
8
+ def test_message_created_predicate
9
+ assert UpdateHelpers.message_created?(update_type: 'message_created')
10
+ refute UpdateHelpers.message_created?(update_type: 'other')
11
+ end
12
+
13
+ def test_message_text_from_body_hash
14
+ msg = { body: { text: 'hello' } }
15
+ assert_equal 'hello', UpdateHelpers.message_text(msg)
16
+ end
17
+
18
+ def test_message_destination_chat_id
19
+ msg = { recipient: { chat_id: -100 } }
20
+ assert_equal [:chat_id, -100], UpdateHelpers.message_destination(msg)
21
+ end
22
+
23
+ def test_message_destination_user_id_fallback
24
+ msg = { recipient: { user_id: 42 } }
25
+ assert_equal [:user_id, 42], UpdateHelpers.message_destination(msg)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ module Max
6
+ module Bot
7
+ class WebhookTest < Minitest::Test
8
+ def test_secret_valid_when_no_secret_configured
9
+ assert Webhook.secret_valid?('anything', nil)
10
+ assert Webhook.secret_valid?('anything', '')
11
+ end
12
+
13
+ def test_secret_valid_compares_constant_time
14
+ assert Webhook.secret_valid?('abc', 'abc')
15
+ refute Webhook.secret_valid?('abc', 'abd')
16
+ refute Webhook.secret_valid?('abc', 'ab')
17
+ end
18
+
19
+ def test_extract_secret_header_rack_style
20
+ env = { 'HTTP_X_MAX_BOT_API_SECRET' => 'sekret' }
21
+ assert_equal 'sekret', Webhook.extract_secret_header(env)
22
+ end
23
+
24
+ def test_parse_json_empty
25
+ assert_equal({}, Webhook.parse_json(nil))
26
+ assert_equal({}, Webhook.parse_json(''))
27
+ end
28
+
29
+ def test_parse_json_object
30
+ h = Webhook.parse_json('{"update_type":"message_created"}')
31
+ assert_equal 'message_created', h[:update_type]
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
4
+
5
+ require 'max_bot'
6
+ require 'minitest/autorun'
metadata CHANGED
@@ -1,23 +1,113 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: max_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergey Syabrenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-22 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Max messenger bot
11
+ date: 2026-04-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '1.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: minitest
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '5.0'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '7'
43
+ type: :development
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '5.0'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '7'
53
+ - !ruby/object:Gem::Dependency
54
+ name: rake
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '13.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '15'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '13.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '15'
73
+ description: Send messages, long-poll GET /updates, manage webhook subscriptions.
74
+ See https://dev.max.ru/docs-api
14
75
  email: darthpains@gmail.com
15
76
  executables: []
16
77
  extensions: []
17
78
  extra_rdoc_files: []
18
79
  files:
80
+ - CHANGELOG.md
81
+ - LICENSE.txt
82
+ - README.md
83
+ - Rakefile
84
+ - examples/polling_bot.rb
85
+ - lib/max/bot.rb
86
+ - lib/max/bot/api.rb
87
+ - lib/max/bot/api/request_builders.rb
88
+ - lib/max/bot/attachments.rb
89
+ - lib/max/bot/client.rb
90
+ - lib/max/bot/configuration.rb
91
+ - lib/max/bot/errors.rb
92
+ - lib/max/bot/http.rb
93
+ - lib/max/bot/json.rb
94
+ - lib/max/bot/multipart_upload.rb
95
+ - lib/max/bot/update_helpers.rb
96
+ - lib/max/bot/version.rb
97
+ - lib/max/bot/webhook.rb
19
98
  - lib/max_bot.rb
20
- homepage: https://rubygems.org/gems/max_bot
99
+ - test/max/bot/api/request_builders_test.rb
100
+ - test/max/bot/api_send_test.rb
101
+ - test/max/bot/api_test.rb
102
+ - test/max/bot/attachments_test.rb
103
+ - test/max/bot/client_test.rb
104
+ - test/max/bot/errors_test.rb
105
+ - test/max/bot/http_test.rb
106
+ - test/max/bot/json_test.rb
107
+ - test/max/bot/update_helpers_test.rb
108
+ - test/max/bot/webhook_test.rb
109
+ - test/test_helper.rb
110
+ homepage: https://github.com/Syabr/max_bot
21
111
  licenses:
22
112
  - MIT
23
113
  metadata: {}
@@ -39,5 +129,5 @@ requirements: []
39
129
  rubygems_version: 3.5.22
40
130
  signing_key:
41
131
  specification_version: 4
42
- summary: Max messenger bot
132
+ summary: Ruby client for the MAX messenger Bot API
43
133
  test_files: []