AtlatestRepositorysigil-lemonsqueezy

sigil-lemonsqueezy / tree / testlemonsqueezy-test.sgl

1;;; Tests for sigil-lemonsqueezy
2;;;
3;;; Tests JSON:API parsing, record construction, webhook verification,
4;;; and query parameter building using response fixtures.
5
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 samples
23;; ---------------------------------------------------------------
25(define product-response-json
26 (json-decode
27 (string-append
28 "{\"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-json
51 (json-decode
52 (string-append
53 "{\"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-json
73 (json-decode
74 (string-append
75 "{\"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-json
91 (json-decode
92 (string-append
93 "{\"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-json
115 (json-decode
116 (string-append
117 "{\"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-json
131 (json-decode
132 (string-append
133 "{\"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-json
161 (json-decode
162 (string-append
163 "{\"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-json
176 (json-decode
177 (string-append
178 "{\"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-json
190 (string-append
191 "{\"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-json
207 (json-decode
208 (string-append
209 "{\"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-json
227 (json-decode
228 (string-append
229 "{\"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-json
254 (json-decode
255 (string-append
256 "{\"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 tests
273;; ---------------------------------------------------------------
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 tests
340;; ---------------------------------------------------------------
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 tests
356;; ---------------------------------------------------------------
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 tests
380;; ---------------------------------------------------------------
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 tests
399;; ---------------------------------------------------------------
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 tests
419;; ---------------------------------------------------------------
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 tests
437;; ---------------------------------------------------------------
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 fixture
458 (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 tests
472;; ---------------------------------------------------------------
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 null
485 (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 tests
491;; ---------------------------------------------------------------
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 data
510 (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 ;; Data
515 (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 tests
526;; ---------------------------------------------------------------
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 tests
555;; ---------------------------------------------------------------
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 null
572 (assert-equal #f (ls-license-key-expires-at lk)))))
574;; ---------------------------------------------------------------
575;; License validation parsing tests
576;; ---------------------------------------------------------------
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 info
586 (let ((lk (ls-license-validation-license-key v)))
587 (assert-true (dict? lk))
588 (assert-equal "active" (dict-ref lk status:)))
589 ;; Instance
590 (let ((inst (ls-license-validation-instance v)))
591 (assert-true (dict? inst))
592 (assert-equal "mysite.com" (dict-ref inst name:)))
593 ;; Meta
594 (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 tests
601;; ---------------------------------------------------------------
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 tests
617;; ---------------------------------------------------------------
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 tests
644;; ---------------------------------------------------------------
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: 5000
657 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)