AtlatestRepositorysigil-lemonsqueezy
sigil-lemonsqueezy / tree / testlemonsqueezy-test.sgl
1
;;; Tests for sigil-lemonsqueezy2
;;;3
;;; Tests JSON:API parsing, record construction, webhook verification,4
;;; and query parameter building using response fixtures.6
(import (sigil core)7
(sigil dict)8
(sigil string)9
(sigil struct)10
(sigil json)11
(sigil crypto)12
(sigil test)13
(only (sigil http) build-query-string)14
(lemonsqueezy)15
(lemonsqueezy product)16
(lemonsqueezy checkout)17
(lemonsqueezy order)18
(lemonsqueezy webhook)19
(lemonsqueezy license))21
;; ---------------------------------------------------------------22
;; Test fixtures — JSON:API response samples23
;; ---------------------------------------------------------------25
(define product-response-json26
(json-decode27
(string-append28
"{\"data\": {\"type\": \"products\", \"id\": \"1\","29
" \"attributes\": {"30
" \"store_id\": 1,"31
" \"name\": \"Course Bundle\","32
" \"slug\": \"course-bundle\","33
" \"description\": \"<p>All courses</p>\","34
" \"status\": \"published\","35
" \"price\": 9999,"36
" \"price_formatted\": \"$99.99\","37
" \"buy_now_url\": \"https://store.example.com/buy/1\","38
" \"test_mode\": false,"39
" \"created_at\": \"2026-01-15T10:30:00.000000Z\","40
" \"updated_at\": \"2026-03-01T08:00:00.000000Z\""41
" },"42
" \"relationships\": {"43
" \"store\": {\"data\": {\"type\": \"stores\", \"id\": \"1\"}},"44
" \"variants\": {\"data\": ["45
" {\"type\": \"variants\", \"id\": \"10\"},"46
" {\"type\": \"variants\", \"id\": \"11\"}"47
" ]}"48
" }}}")))50
(define products-list-json51
(json-decode52
(string-append53
"{\"data\": ["54
" {\"type\": \"products\", \"id\": \"1\","55
" \"attributes\": {\"name\": \"Course A\", \"status\": \"published\","56
" \"price\": 4999, \"price_formatted\": \"$49.99\","57
" \"store_id\": 1, \"test_mode\": false,"58
" \"created_at\": \"2026-01-01T00:00:00.000000Z\","59
" \"updated_at\": \"2026-01-01T00:00:00.000000Z\"}},"60
" {\"type\": \"products\", \"id\": \"2\","61
" \"attributes\": {\"name\": \"Course B\", \"status\": \"draft\","62
" \"price\": 2999, \"price_formatted\": \"$29.99\","63
" \"store_id\": 1, \"test_mode\": true,"64
" \"created_at\": \"2026-02-01T00:00:00.000000Z\","65
" \"updated_at\": \"2026-02-01T00:00:00.000000Z\"}}"66
"],"67
"\"meta\": {\"page\": {\"currentPage\": 1, \"lastPage\": 1,"68
" \"perPage\": 10, \"total\": 2, \"from\": 1, \"to\": 2}},"69
"\"links\": {\"first\": \"https://api.lemonsqueezy.com/v1/products?page%5Bnumber%5D=1\","70
" \"last\": \"https://api.lemonsqueezy.com/v1/products?page%5Bnumber%5D=1\"}}")))72
(define variant-response-json73
(json-decode74
(string-append75
"{\"data\": {\"type\": \"variants\", \"id\": \"10\","76
" \"attributes\": {"77
" \"product_id\": 1,"78
" \"name\": \"Standard\","79
" \"slug\": \"standard\","80
" \"description\": \"Standard tier\","81
" \"price\": 4999,"82
" \"sort\": 1,"83
" \"status\": \"published\","84
" \"has_license_keys\": true,"85
" \"license_activation_limit\": 3,"86
" \"created_at\": \"2026-01-15T10:30:00.000000Z\","87
" \"updated_at\": \"2026-03-01T08:00:00.000000Z\""88
" }}}")))90
(define order-response-json91
(json-decode92
(string-append93
"{\"data\": {\"type\": \"orders\", \"id\": \"100\","94
" \"attributes\": {"95
" \"store_id\": 1,"96
" \"customer_id\": 50,"97
" \"identifier\": \"a1b2c3d4-e5f6-7890-abcd-ef1234567890\","98
" \"order_number\": 42,"99
" \"user_name\": \"Alice Smith\","100
" \"user_email\": \"[email protected]\","101
" \"currency\": \"USD\","102
" \"subtotal\": 9999,"103
" \"discount_total\": 0,"104
" \"tax\": 850,"105
" \"total\": 10849,"106
" \"total_formatted\": \"$108.49\","107
" \"status\": \"paid\","108
" \"refunded\": false,"109
" \"test_mode\": false,"110
" \"created_at\": \"2026-03-20T14:00:00.000000Z\","111
" \"updated_at\": \"2026-03-20T14:00:00.000000Z\""112
" }}}")))114
(define order-item-json115
(json-decode116
(string-append117
"{\"data\": {\"type\": \"order-items\", \"id\": \"200\","118
" \"attributes\": {"119
" \"order_id\": 100,"120
" \"product_id\": 1,"121
" \"variant_id\": 10,"122
" \"product_name\": \"Course Bundle\","123
" \"variant_name\": \"Standard\","124
" \"price\": 9999,"125
" \"quantity\": 1,"126
" \"created_at\": \"2026-03-20T14:00:00.000000Z\","127
" \"updated_at\": \"2026-03-20T14:00:00.000000Z\""128
" }}}")))130
(define subscription-response-json131
(json-decode132
(string-append133
"{\"data\": {\"type\": \"subscriptions\", \"id\": \"300\","134
" \"attributes\": {"135
" \"store_id\": 1,"136
" \"customer_id\": 50,"137
" \"order_id\": 100,"138
" \"product_id\": 1,"139
" \"variant_id\": 10,"140
" \"product_name\": \"Pro Plan\","141
" \"variant_name\": \"Monthly\","142
" \"user_name\": \"Alice Smith\","143
" \"user_email\": \"[email protected]\","144
" \"status\": \"active\","145
" \"card_brand\": \"visa\","146
" \"card_last_four\": \"4242\","147
" \"trial_ends_at\": null,"148
" \"renews_at\": \"2026-04-20T14:00:00.000000Z\","149
" \"ends_at\": null,"150
" \"cancelled\": false,"151
" \"test_mode\": false,"152
" \"created_at\": \"2026-03-20T14:00:00.000000Z\","153
" \"updated_at\": \"2026-03-20T14:00:00.000000Z\","154
" \"urls\": {"155
" \"update_payment_method\": \"https://example.lemonsqueezy.com/billing\","156
" \"customer_portal\": \"https://example.lemonsqueezy.com/portal\""157
" }"158
" }}}")))160
(define checkout-response-json161
(json-decode162
(string-append163
"{\"data\": {\"type\": \"checkouts\", \"id\": \"ch_abc123\","164
" \"attributes\": {"165
" \"store_id\": 1,"166
" \"variant_id\": 10,"167
" \"url\": \"https://example.lemonsqueezy.com/checkout/buy/abc123\","168
" \"custom_price\": null,"169
" \"expires_at\": \"2026-04-01T00:00:00.000000Z\","170
" \"created_at\": \"2026-03-25T10:00:00.000000Z\","171
" \"updated_at\": \"2026-03-25T10:00:00.000000Z\","172
" \"test_mode\": true"173
" }}}")))175
(define webhook-resource-json176
(json-decode177
(string-append178
"{\"data\": {\"type\": \"webhooks\", \"id\": \"500\","179
" \"attributes\": {"180
" \"store_id\": 1,"181
" \"url\": \"https://myapp.example.com/webhooks/ls\","182
" \"events\": [\"order_created\", \"subscription_created\","183
" \"subscription_expired\"],"184
" \"test_mode\": false,"185
" \"created_at\": \"2026-03-20T10:00:00.000000Z\","186
" \"updated_at\": \"2026-03-20T10:00:00.000000Z\""187
" }}}")))189
(define webhook-payload-json190
(string-append191
"{\"meta\": {"192
" \"event_name\": \"order_created\","193
" \"custom_data\": {\"user_id\": \"123\", \"plan\": \"pro\"}"194
"},"195
"\"data\": {"196
" \"type\": \"orders\","197
" \"id\": \"100\","198
" \"attributes\": {"199
" \"store_id\": 1,"200
" \"user_email\": \"[email protected]\","201
" \"status\": \"paid\","202
" \"total\": 9999"203
" }"204
"}}"))206
(define license-key-json207
(json-decode208
(string-append209
"{\"data\": {\"type\": \"license-keys\", \"id\": \"700\","210
" \"attributes\": {"211
" \"store_id\": 1,"212
" \"customer_id\": 50,"213
" \"order_id\": 100,"214
" \"product_id\": 1,"215
" \"key\": \"38b1460a-5104-4067-a91d-77b872934d51\","216
" \"key_short\": \"XXXX-72934d51\","217
" \"activation_limit\": 3,"218
" \"instances_count\": 1,"219
" \"disabled\": false,"220
" \"status\": \"active\","221
" \"expires_at\": null,"222
" \"created_at\": \"2026-03-20T14:00:00.000000Z\","223
" \"updated_at\": \"2026-03-20T14:00:00.000000Z\""224
" }}}")))226
(define license-validation-json227
(json-decode228
(string-append229
"{\"valid\": true, \"error\": null,"230
" \"license_key\": {"231
" \"id\": 700,"232
" \"status\": \"active\","233
" \"key\": \"38b1460a-5104-4067-a91d-77b872934d51\","234
" \"activation_limit\": 3,"235
" \"activation_usage\": 1"236
" },"237
" \"instance\": {"238
" \"id\": \"f90ec370-1234-5678-9abc-def012345678\","239
" \"name\": \"mysite.com\","240
" \"created_at\": \"2026-03-20T14:00:00.000000Z\""241
" },"242
" \"meta\": {"243
" \"store_id\": 1,"244
" \"order_id\": 100,"245
" \"product_id\": 1,"246
" \"variant_id\": 10,"247
" \"product_name\": \"Course Bundle\","248
" \"variant_name\": \"Standard\","249
" \"customer_name\": \"Alice Smith\","250
" \"customer_email\": \"[email protected]\""251
" }}")))253
(define included-response-json254
(json-decode255
(string-append256
"{\"data\": {\"type\": \"products\", \"id\": \"1\","257
" \"attributes\": {\"name\": \"Course Bundle\"},"258
" \"relationships\": {"259
" \"variants\": {\"data\": ["260
" {\"type\": \"variants\", \"id\": \"10\"},"261
" {\"type\": \"variants\", \"id\": \"11\"}"262
" ]}"263
" }},"264
"\"included\": ["265
" {\"type\": \"variants\", \"id\": \"10\","266
" \"attributes\": {\"name\": \"Standard\", \"price\": 4999}},"267
" {\"type\": \"variants\", \"id\": \"11\","268
" \"attributes\": {\"name\": \"Premium\", \"price\": 9999}}"269
"]}")))271
;; ---------------------------------------------------------------272
;; JSON:API helper tests273
;; ---------------------------------------------------------------275
(test-group "JSON:API helpers"277
(test "jsonapi-id extracts resource id"278
(let ((data (jsonapi-data product-response-json)))279
(assert-equal "1" (jsonapi-id data))))281
(test "jsonapi-type extracts resource type"282
(let ((data (jsonapi-data product-response-json)))283
(assert-equal "products" (jsonapi-type data))))285
(test "jsonapi-attr extracts attribute"286
(let ((data (jsonapi-data product-response-json)))287
(assert-equal "Course Bundle" (jsonapi-attr data name:))288
(assert-equal 9999 (jsonapi-attr data price:))289
(assert-equal "published" (jsonapi-attr data status:))))291
(test "jsonapi-attr returns default for missing attribute"292
(let ((data (jsonapi-data product-response-json)))293
(assert-equal "default" (jsonapi-attr data nonexistent: "default"))))295
(test "jsonapi-attrs returns full attributes dict"296
(let ((attrs (jsonapi-attrs (jsonapi-data product-response-json))))297
(assert-true (dict? attrs))298
(assert-equal "Course Bundle" (dict-ref attrs name:))))300
(test "jsonapi-relationship-id extracts single relationship"301
(let ((data (jsonapi-data product-response-json)))302
(assert-equal "1" (jsonapi-relationship-id data store:))))304
(test "jsonapi-relationship-ids extracts has-many relationship"305
(let ((data (jsonapi-data product-response-json)))306
(let ((ids (jsonapi-relationship-ids data variants:)))307
(assert-equal 2 (length ids))308
(assert-equal "10" (car ids))309
(assert-equal "11" (cadr ids)))))311
(test "jsonapi-data-list extracts list from array data"312
(let ((items (jsonapi-data-list products-list-json)))313
(assert-equal 2 (length items))314
(assert-equal "1" (jsonapi-id (car items)))315
(assert-equal "2" (jsonapi-id (cadr items)))))317
(test "jsonapi-included extracts included resources"318
(let ((included (jsonapi-included included-response-json)))319
(assert-equal 2 (length included))320
(assert-equal "variants" (jsonapi-type (car included)))))322
(test "jsonapi-find-included finds by type and id"323
(let ((variant (jsonapi-find-included included-response-json "variants" "11")))324
(assert-true (dict? variant))325
(assert-equal "11" (jsonapi-id variant))326
(assert-equal "Premium" (jsonapi-attr variant name:))))328
(test "jsonapi-find-included returns #f for missing"329
(assert-equal #f (jsonapi-find-included included-response-json "variants" "999")))331
(test "jsonapi-pagination-meta extracts page info"332
(let ((meta (jsonapi-pagination-meta products-list-json)))333
(assert-equal 1 (dict-ref meta current-page:))334
(assert-equal 1 (dict-ref meta last-page:))335
(assert-equal 10 (dict-ref meta per-page:))336
(assert-equal 2 (dict-ref meta total:)))))338
;; ---------------------------------------------------------------339
;; maybe-null tests340
;; ---------------------------------------------------------------342
(test-group "maybe-null"344
(test "normalizes null symbol to #f"345
(assert-equal #f (maybe-null 'null)))347
(test "passes through #f"348
(assert-equal #f (maybe-null #f)))350
(test "passes through truthy values"351
(assert-equal "hello" (maybe-null "hello"))352
(assert-equal 42 (maybe-null 42))))354
;; ---------------------------------------------------------------355
;; Product parsing tests356
;; ---------------------------------------------------------------358
(test-group "product parsing"360
(test "parse single product"361
(let ((p (parse-product (jsonapi-data product-response-json))))362
(assert-true (ls-product? p))363
(assert-equal "1" (ls-product-id p))364
(assert-equal "Course Bundle" (ls-product-name p))365
(assert-equal "course-bundle" (ls-product-slug p))366
(assert-equal "published" (ls-product-status p))367
(assert-equal 9999 (ls-product-price p))368
(assert-equal "$99.99" (ls-product-price-formatted p))369
(assert-equal "https://store.example.com/buy/1" (ls-product-buy-now-url p))))371
(test "parse product list"372
(let ((products (map parse-product (jsonapi-data-list products-list-json))))373
(assert-equal 2 (length products))374
(assert-equal "Course A" (ls-product-name (car products)))375
(assert-equal "Course B" (ls-product-name (cadr products)))376
(assert-equal "draft" (ls-product-status (cadr products))))))378
;; ---------------------------------------------------------------379
;; Variant parsing tests380
;; ---------------------------------------------------------------382
(test-group "variant parsing"384
(test "parse single variant"385
(let ((v (parse-variant (jsonapi-data variant-response-json))))386
(assert-true (ls-variant? v))387
(assert-equal "10" (ls-variant-id v))388
(assert-equal "Standard" (ls-variant-name v))389
(assert-equal "standard" (ls-variant-slug v))390
(assert-equal 4999 (ls-variant-price v))391
(assert-equal 1 (ls-variant-sort v))392
(assert-equal "published" (ls-variant-status v))393
(assert-equal 1 (ls-variant-product-id v))394
(assert-equal #t (ls-variant-has-license-keys v))395
(assert-equal 3 (ls-variant-license-activation-limit v)))))397
;; ---------------------------------------------------------------398
;; Order parsing tests399
;; ---------------------------------------------------------------401
(test-group "order parsing"403
(test "parse single order"404
(let ((o (parse-order (jsonapi-data order-response-json))))405
(assert-true (ls-order? o))406
(assert-equal "100" (ls-order-id o))407
(assert-equal 1 (ls-order-store-id o))408
(assert-equal 50 (ls-order-customer-id o))409
(assert-equal "Alice Smith" (ls-order-user-name o))410
(assert-equal "[email protected]" (ls-order-user-email o))411
(assert-equal "USD" (ls-order-currency o))412
(assert-equal 10849 (ls-order-total o))413
(assert-equal "$108.49" (ls-order-total-formatted o))414
(assert-equal "paid" (ls-order-status o))415
(assert-equal #f (ls-order-refunded o)))))417
;; ---------------------------------------------------------------418
;; Order item parsing tests419
;; ---------------------------------------------------------------421
(test-group "order item parsing"423
(test "parse order item"424
(let ((oi (parse-order-item (jsonapi-data order-item-json))))425
(assert-true (ls-order-item? oi))426
(assert-equal "200" (ls-order-item-id oi))427
(assert-equal 100 (ls-order-item-order-id oi))428
(assert-equal 1 (ls-order-item-product-id oi))429
(assert-equal 10 (ls-order-item-variant-id oi))430
(assert-equal "Course Bundle" (ls-order-item-product-name oi))431
(assert-equal "Standard" (ls-order-item-variant-name oi))432
(assert-equal 9999 (ls-order-item-price oi))433
(assert-equal 1 (ls-order-item-quantity oi)))))435
;; ---------------------------------------------------------------436
;; Subscription parsing tests437
;; ---------------------------------------------------------------439
(test-group "subscription parsing"441
(test "parse subscription"442
(let ((s (parse-subscription (jsonapi-data subscription-response-json))))443
(assert-true (ls-subscription? s))444
(assert-equal "300" (ls-subscription-id s))445
(assert-equal "active" (ls-subscription-status s))446
(assert-equal "Pro Plan" (ls-subscription-product-name s))447
(assert-equal "Monthly" (ls-subscription-variant-name s))448
(assert-equal "Alice Smith" (ls-subscription-user-name s))449
(assert-equal "[email protected]" (ls-subscription-user-email s))450
(assert-equal "visa" (ls-subscription-card-brand s))451
(assert-equal "4242" (ls-subscription-card-last-four s))452
(assert-equal "2026-04-20T14:00:00.000000Z" (ls-subscription-renews-at s))453
(assert-equal #f (ls-subscription-cancelled s))))455
(test "subscription null fields normalized"456
(let ((s (parse-subscription (jsonapi-data subscription-response-json))))457
;; trial_ends_at and ends_at are null in fixture458
(assert-equal #f (ls-subscription-trial-ends-at s))459
(assert-equal #f (ls-subscription-ends-at s))))461
(test "subscription urls preserved"462
(let ((s (parse-subscription (jsonapi-data subscription-response-json))))463
(let ((urls (ls-subscription-urls s)))464
(assert-true (dict? urls))465
(assert-equal "https://example.lemonsqueezy.com/billing"466
(dict-ref urls update_payment_method:))467
(assert-equal "https://example.lemonsqueezy.com/portal"468
(dict-ref urls customer_portal:))))))470
;; ---------------------------------------------------------------471
;; Checkout parsing tests472
;; ---------------------------------------------------------------474
(test-group "checkout parsing"476
(test "parse checkout"477
(let ((c (parse-checkout (jsonapi-data checkout-response-json))))478
(assert-true (ls-checkout? c))479
(assert-equal "ch_abc123" (ls-checkout-id c))480
(assert-equal "https://example.lemonsqueezy.com/checkout/buy/abc123"481
(ls-checkout-url c))482
(assert-equal 1 (ls-checkout-store-id c))483
(assert-equal 10 (ls-checkout-variant-id c))484
;; custom_price is null485
(assert-equal #f (ls-checkout-custom-price c))486
(assert-equal "2026-04-01T00:00:00.000000Z" (ls-checkout-expires-at c))487
(assert-equal #t (ls-checkout-test-mode c)))))489
;; ---------------------------------------------------------------490
;; Webhook parsing tests491
;; ---------------------------------------------------------------493
(test-group "webhook parsing"495
(test "parse webhook resource"496
(let ((w (parse-webhook (jsonapi-data webhook-resource-json))))497
(assert-true (ls-webhook? w))498
(assert-equal "500" (ls-webhook-id w))499
(assert-equal "https://myapp.example.com/webhooks/ls" (ls-webhook-url w))500
(assert-equal 3 (length (ls-webhook-events w)))501
(assert-true (member "order_created" (ls-webhook-events w)))502
(assert-true (member "subscription_created" (ls-webhook-events w)))503
(assert-true (member "subscription_expired" (ls-webhook-events w)))))505
(test "parse webhook event payload"506
(let ((ev (parse-webhook-event webhook-payload-json)))507
(assert-true (ls-webhook-event? ev))508
(assert-equal "order_created" (ls-webhook-event-name ev))509
;; Custom data510
(let ((cd (ls-webhook-event-custom-data ev)))511
(assert-true (dict? cd))512
(assert-equal "123" (dict-ref cd user_id:))513
(assert-equal "pro" (dict-ref cd plan:)))514
;; Data515
(let ((data (ls-webhook-event-data ev)))516
(assert-equal "orders" (dict-ref data type:))517
(assert-equal "100" (dict-ref data id:)))))519
(test "parse webhook event from dict"520
(let* ((parsed (json-decode webhook-payload-json))521
(ev (parse-webhook-event parsed)))522
(assert-equal "order_created" (ls-webhook-event-name ev)))))524
;; ---------------------------------------------------------------525
;; Webhook verification tests526
;; ---------------------------------------------------------------528
(test-group "webhook verification"530
(test "valid signature passes verification"531
(let* ((secret "my-webhook-secret")532
(body webhook-payload-json)533
(signature (hmac-sha256 secret body)))534
(assert-true (ls-verify-webhook secret body signature))))536
(test "invalid signature fails verification"537
(let* ((secret "my-webhook-secret")538
(body webhook-payload-json))539
(assert-equal #f (ls-verify-webhook secret body "deadbeef0000"))))541
(test "wrong secret fails verification"542
(let* ((body webhook-payload-json)543
(signature (hmac-sha256 "correct-secret" body)))544
(assert-equal #f (ls-verify-webhook "wrong-secret" body signature))))546
(test "tampered body fails verification"547
(let* ((secret "my-webhook-secret")548
(body webhook-payload-json)549
(signature (hmac-sha256 secret body))550
(tampered (string-append body " ")))551
(assert-equal #f (ls-verify-webhook secret tampered signature)))))553
;; ---------------------------------------------------------------554
;; License key parsing tests555
;; ---------------------------------------------------------------557
(test-group "license key parsing"559
(test "parse license key from JSON:API"560
(let ((lk (parse-license-key (jsonapi-data license-key-json))))561
(assert-true (ls-license-key? lk))562
(assert-equal "700" (ls-license-key-id lk))563
(assert-equal 1 (ls-license-key-store-id lk))564
(assert-equal 50 (ls-license-key-customer-id lk))565
(assert-equal "38b1460a-5104-4067-a91d-77b872934d51" (ls-license-key-key lk))566
(assert-equal "XXXX-72934d51" (ls-license-key-key-short lk))567
(assert-equal 3 (ls-license-key-activation-limit lk))568
(assert-equal 1 (ls-license-key-instances-count lk))569
(assert-equal #f (ls-license-key-disabled lk))570
(assert-equal "active" (ls-license-key-status lk))571
;; expires_at is null572
(assert-equal #f (ls-license-key-expires-at lk)))))574
;; ---------------------------------------------------------------575
;; License validation parsing tests576
;; ---------------------------------------------------------------578
(test-group "license validation parsing"580
(test "parse license validation response"581
(let ((v (parse-license-validation license-validation-json)))582
(assert-true (ls-license-validation? v))583
(assert-equal #t (ls-license-validation-valid v))584
(assert-equal #f (ls-license-validation-error v))585
;; License key info586
(let ((lk (ls-license-validation-license-key v)))587
(assert-true (dict? lk))588
(assert-equal "active" (dict-ref lk status:)))589
;; Instance590
(let ((inst (ls-license-validation-instance v)))591
(assert-true (dict? inst))592
(assert-equal "mysite.com" (dict-ref inst name:)))593
;; Meta594
(let ((meta (ls-license-validation-meta v)))595
(assert-true (dict? meta))596
(assert-equal "Course Bundle" (dict-ref meta product_name:))597
(assert-equal "[email protected]" (dict-ref meta customer_email:))))))599
;; ---------------------------------------------------------------600
;; Client construction tests601
;; ---------------------------------------------------------------603
(test-group "client construction"605
(test "default base URL"606
(let ((c (ls-client api-key: "test-key-123")))607
(assert-true (ls-client? c))608
(assert-equal "test-key-123" (ls-client-api-key c))609
(assert-equal "https://api.lemonsqueezy.com" (ls-client-base-url c))))611
(test "custom base URL"612
(let ((c (ls-client api-key: "key" base-url: "https://custom.api.com")))613
(assert-equal "https://custom.api.com" (ls-client-base-url c)))))615
;; ---------------------------------------------------------------616
;; Query parameter building tests617
;; ---------------------------------------------------------------619
(test-group "list params building"621
(test "empty opts returns params with #f values"622
(let ((params (ls-list-params)))623
(assert-true (list? params))))625
(test "page and per-page params"626
(let ((params (ls-list-params #{ page: 2 per-page: 25 })))627
(let ((qs (build-query-string params)))628
(assert-true (string-contains? qs "page%5Bnumber%5D=2"))629
(assert-true (string-contains? qs "page%5Bsize%5D=25")))))631
(test "filter params"632
(let ((params (ls-list-params #{ filter: #{ store_id: "1" status: "active" } })))633
(let ((qs (build-query-string params)))634
(assert-true (string-contains? qs "filter%5Bstore_id%5D=1"))635
(assert-true (string-contains? qs "filter%5Bstatus%5D=active")))))637
(test "include param"638
(let ((params (ls-list-params #{ include: "variants,store" })))639
(let ((qs (build-query-string params)))640
(assert-true (string-contains? qs "include=variants"))))))642
;; ---------------------------------------------------------------643
;; Record construction tests644
;; ---------------------------------------------------------------646
(test-group "record construction"648
(test "ls-product fields"649
(let ((p (ls-product id: "1" name: "Test" status: "published" price: 999)))650
(assert-equal "1" (ls-product-id p))651
(assert-equal "Test" (ls-product-name p))652
(assert-equal "published" (ls-product-status p))653
(assert-equal 999 (ls-product-price p))))655
(test "ls-order fields"656
(let ((o (ls-order id: "100" status: "paid" total: 5000657
user-email: "[email protected]")))658
(assert-equal "100" (ls-order-id o))659
(assert-equal "paid" (ls-order-status o))660
(assert-equal 5000 (ls-order-total o))661
(assert-equal "[email protected]" (ls-order-user-email o))))663
(test "ls-subscription fields"664
(let ((s (ls-subscription id: "300" status: "active"665
renews-at: "2026-04-20T00:00:00Z")))666
(assert-equal "300" (ls-subscription-id s))667
(assert-equal "active" (ls-subscription-status s))668
(assert-equal "2026-04-20T00:00:00Z" (ls-subscription-renews-at s))))670
(test "ls-webhook-event fields"671
(let ((ev (ls-webhook-event name: "order_created"672
custom-data: #{ user_id: "42" })))673
(assert-equal "order_created" (ls-webhook-event-name ev))674
(assert-equal "42" (dict-ref (ls-webhook-event-custom-data ev) user_id:))))676
(test "ls-license-key fields"677
(let ((lk (ls-license-key id: "700" key: "abc-123" status: "active"678
activation-limit: 5)))679
(assert-equal "700" (ls-license-key-id lk))680
(assert-equal "abc-123" (ls-license-key-key lk))681
(assert-equal "active" (ls-license-key-status lk))682
(assert-equal 5 (ls-license-key-activation-limit lk)))))684
(run-tests)