AtlatestRepositorysigil-telegram
sigil-telegram / tree / testtest-telegram.sgl
1
(import (sigil test)2
(sigil io)3
(sigil string)4
(sigil telegram types)5
(sigil telegram client)6
(sigil telegram bot)7
(sigil telegram format))9
;; ============================================================10
;; Type Parsing11
;; ============================================================13
(test-group "dict->tg-user"14
(test "parses all fields"15
(let ((user (dict->tg-user #{ id: 123 is_bot: #f16
first_name: "Alice"17
last_name: "Smith"18
username: "alice" })))19
(assert-equal 123 (tg-user-id user))20
(assert-false (tg-user-is-bot user))21
(assert-equal "Alice" (tg-user-first-name user))22
(assert-equal "Smith" (tg-user-last-name user))23
(assert-equal "alice" (tg-user-username user))))25
(test "handles missing optional fields"26
(let ((user (dict->tg-user #{ id: 456 is_bot: #t first_name: "Bot" })))27
(assert-equal 456 (tg-user-id user))28
(assert-true (tg-user-is-bot user))29
(assert-false (tg-user-last-name user))30
(assert-false (tg-user-username user)))))32
(test-group "dict->tg-chat"33
(test "parses private chat"34
(let ((chat (dict->tg-chat #{ id: 100 type: "private"35
username: "alice" })))36
(assert-equal 100 (tg-chat-id chat))37
(assert-equal "private" (tg-chat-type chat))38
(assert-false (tg-chat-title chat))39
(assert-equal "alice" (tg-chat-username chat))))41
(test "parses group chat"42
(let ((chat (dict->tg-chat #{ id: -200 type: "supergroup"43
title: "My Group" })))44
(assert-equal -200 (tg-chat-id chat))45
(assert-equal "supergroup" (tg-chat-type chat))46
(assert-equal "My Group" (tg-chat-title chat)))))48
(test-group "dict->tg-message"49
(test "parses text message"50
(let ((msg (dict->tg-message51
#{ message_id: 4252
chat: #{ id: 100 type: "private" }53
from: #{ id: 123 is_bot: #f first_name: "Alice" }54
date: 170000000055
text: "Hello world" })))56
(assert-equal 42 (tg-message-message-id msg))57
(assert-equal 100 (tg-chat-id (tg-message-chat msg)))58
(assert-equal 123 (tg-user-id (tg-message-from msg)))59
(assert-equal 1700000000 (tg-message-date msg))60
(assert-equal "Hello world" (tg-message-text msg))61
(assert-false (tg-message-photo msg))62
(assert-false (tg-message-document msg))))64
(test "handles missing from field"65
(let ((msg (dict->tg-message66
#{ message_id: 167
chat: #{ id: 100 type: "channel" }68
date: 069
text: "Channel post" })))70
(assert-false (tg-message-from msg))))72
(test "preserves raw dict"73
(let* ((raw #{ message_id: 1 chat: #{ id: 1 type: "private" }74
text: "test" sticker: #{ file_id: "abc" } })75
(msg (dict->tg-message raw)))76
(assert-equal "abc" (dict-ref (dict-ref (tg-message-raw msg) sticker:) file_id:)))))78
(test-group "dict->tg-update"79
(test "parses message update"80
(let ((update (dict->tg-update81
#{ update_id: 99982
message: #{ message_id: 183
chat: #{ id: 100 type: "private" }84
text: "Hello" } })))85
(assert-equal 999 (tg-update-update-id update))86
(assert-true (tg-message? (tg-update-message update)))87
(assert-false (tg-update-callback-query update))))89
(test "parses callback query update"90
(let ((update (dict->tg-update91
#{ update_id: 100092
callback_query: #{ id: "abc123"93
data: "btn_yes" } })))94
(assert-equal 1000 (tg-update-update-id update))95
(assert-false (tg-update-message update))96
(assert-equal "abc123" (dict-ref (tg-update-callback-query update) id:)))))98
;; ============================================================99
;; Command Extraction100
;; ============================================================102
(test-group "extract-command"103
(test "simple command"104
(assert-equal "/start" (extract-command "/start")))106
(test "command with bot mention"107
(assert-equal "/help" (extract-command "/help@MyBot")))109
(test "command with arguments"110
(assert-equal "/set" (extract-command "/set value 42")))112
(test "command with mention and arguments"113
(assert-equal "/cmd" (extract-command "/cmd@Bot arg1 arg2")))115
(test "single slash"116
(assert-equal "/" (extract-command "/"))))118
;; ============================================================119
;; MarkdownV2 Escaping120
;; ============================================================122
(test-group "tg-escape-markdown"123
(test "escapes exclamation mark"124
(assert-equal "hello\\!" (tg-escape-markdown "hello!")))126
(test "escapes period"127
(assert-equal "1\\.0" (tg-escape-markdown "1.0")))129
(test "escapes multiple characters"130
(assert-equal "a\\.b\\~c" (tg-escape-markdown "a.b~c")))132
(test "escapes parentheses"133
(assert-equal "\\(test\\)" (tg-escape-markdown "(test)")))135
(test "plain text unchanged"136
(assert-equal "hello world" (tg-escape-markdown "hello world")))138
(test "empty string"139
(assert-equal "" (tg-escape-markdown ""))))141
;; ============================================================142
;; Formatting Helpers143
;; ============================================================145
(test-group "formatting"146
(test "bold"147
(assert-equal "*text*" (tg-bold "text")))149
(test "italic"150
(assert-equal "_text_" (tg-italic "text")))152
(test "underline"153
(assert-equal "__text__" (tg-underline "text")))155
(test "strikethrough"156
(assert-equal "~text~" (tg-strike "text")))158
(test "code"159
(assert-equal "`code`" (tg-code "code")))161
(test "code block without language"162
(assert-equal "```\n(+ 1 2)```" (tg-code-block "(+ 1 2)")))164
(test "code block with language"165
(assert-equal "```scheme\n(+ 1 2)```"166
(tg-code-block "(+ 1 2)" language: "scheme")))168
(test "link"169
(assert-equal "[Click](https://example.com)"170
(tg-link "Click" "https://example.com")))172
(test "spoiler"173
(assert-equal "||hidden||" (tg-spoiler "hidden"))))175
;; ============================================================176
;; Inline Keyboard177
;; ============================================================179
(test-group "inline keyboard"180
(test "button with callback"181
(let ((btn (tg-button "Yes" callback: "yes")))182
(assert-equal "Yes" (dict-ref btn text:))183
(assert-equal "yes" (dict-ref btn callback_data:))))185
(test "button with url"186
(let ((btn (tg-button "Visit" url: "https://example.com")))187
(assert-equal "Visit" (dict-ref btn text:))188
(assert-equal "https://example.com" (dict-ref btn url:))))190
(test "keyboard structure"191
(let ((kb (tg-inline-keyboard192
(list (list (tg-button "A" callback: "a")193
(tg-button "B" callback: "b"))))))194
(assert-true (dict? kb))195
(assert-true (array? (dict-ref kb inline_keyboard:)))196
(assert-equal 1 (array-length (dict-ref kb inline_keyboard:)))197
(assert-equal 2 (array-length (array-ref (dict-ref kb inline_keyboard:) 0))))))199
;; ============================================================200
;; Authorization201
;; ============================================================203
(test-group "authorization"204
(define (make-test-bot (keys: (allowed-chats #f) (allowed-users #f)))205
(tg-bot206
client: (tg-client token: "test")207
allowed-chats: allowed-chats208
allowed-users: allowed-users))210
(define (make-test-update chat-id user-id)211
(tg-update212
update-id: 1213
message: (tg-message214
message-id: 1215
chat: (tg-chat id: chat-id type: "private")216
from: (tg-user id: user-id first-name: "Test"))))218
(test "all allowed when no restrictions"219
(let ((bot (make-test-bot)))220
(assert-true (authorized? bot (make-test-update 100 200)))))222
(test "chat restriction allows matching chat"223
(let ((bot (make-test-bot allowed-chats: '(100 200))))224
(assert-true (authorized? bot (make-test-update 100 999)))))226
(test "chat restriction blocks non-matching chat"227
(let ((bot (make-test-bot allowed-chats: '(100 200))))228
(assert-false (authorized? bot (make-test-update 300 999)))))230
(test "user restriction allows matching user"231
(let ((bot (make-test-bot allowed-users: '(200 300))))232
(assert-true (authorized? bot (make-test-update 999 200)))))234
(test "user restriction blocks non-matching user"235
(let ((bot (make-test-bot allowed-users: '(200 300))))236
(assert-false (authorized? bot (make-test-update 999 400)))))238
(test "both restrictions must pass"239
(let ((bot (make-test-bot allowed-chats: '(100) allowed-users: '(200))))240
(assert-true (authorized? bot (make-test-update 100 200)))241
(assert-false (authorized? bot (make-test-update 100 300)))242
(assert-false (authorized? bot (make-test-update 200 200)))))244
(test "non-message update passes through"245
(let ((bot (make-test-bot allowed-chats: '(100))))246
(assert-true (authorized? bot (tg-update update-id: 1))))))248
;; ============================================================249
;; Multipart Encoding250
;; ============================================================252
(test-group "multipart encoding"253
(test "text part format"254
(let* ((bv (encode-text-part "BOUNDARY" "chat_id" "12345"))255
(str (utf8->string bv)))256
(assert-true (string-contains? str "--BOUNDARY\r\n"))257
(assert-true (string-contains? str "Content-Disposition: form-data; name=\"chat_id\""))258
(assert-true (string-contains? str "12345"))))260
(test "file part format"261
(let* ((data (string->utf8 "fake image data"))262
(bv (encode-file-part "BOUNDARY" "photo" "test.jpg" "image/jpeg" data))263
(str (utf8->string bv)))264
(assert-true (string-contains? str "--BOUNDARY\r\n"))265
(assert-true (string-contains? str "name=\"photo\"; filename=\"test.jpg\""))266
(assert-true (string-contains? str "Content-Type: image/jpeg"))267
(assert-true (string-contains? str "fake image data"))))269
(test "build-multipart-body produces boundary and body"270
(let* ((result (build-multipart-body271
(list (cons "chat_id" "12345"))272
(list (list "photo" "test.jpg" "image/jpeg"273
(string->utf8 "data")))))274
(boundary (car result))275
(body (cdr result)))276
(assert-true (string? boundary))277
(assert-true (bytevector? body))278
;; Body should end with closing boundary279
(let ((str (utf8->string body)))280
(assert-true (string-contains? str (string-append "--" boundary "--"))))))282
(test "video file part uses video field name"283
(let* ((data (string->utf8 "fake video bytes"))284
(bv (encode-file-part "BOUNDARY" "video" "clip.mp4" "video/mp4" data))285
(str (utf8->string bv)))286
(assert-true (string-contains? str "name=\"video\"; filename=\"clip.mp4\""))287
(assert-true (string-contains? str "Content-Type: video/mp4"))288
(assert-true (string-contains? str "fake video bytes"))))290
(test "multipart body preserves arbitrary file bytes verbatim"291
;; Audio tracks and binary container bytes must pass through untouched.292
(let* ((raw (bytevector 0 1 2 255 254 253 0 13 10))293
(result (build-multipart-body294
(list (cons "chat_id" "1"))295
(list (list "video" "a.mp4" "video/mp4" raw))))296
(body (cdr result)))297
(assert-true (bytevector? body))298
;; Locate the raw payload inside the body bytevector299
(let loop ((i 0) (found #f))300
(cond301
(found (assert-true #t))302
((> (+ i (bytevector-length raw)) (bytevector-length body))303
(assert-true #f))304
((let match ((j 0))305
(cond306
((= j (bytevector-length raw)) #t)307
((= (bytevector-u8-ref body (+ i j))308
(bytevector-u8-ref raw j))309
(match (+ j 1)))310
(else #f)))311
(loop i #t))312
(else (loop (+ i 1) #f)))))))314
(run-tests)