;;; async-http-queue-test.el --- Tests for async-http-queue -*- lexical-binding: t -*- ;; Copyright (C) 2025 Andros Fenollosa ;; Author: Andros Fenollosa ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;;; Commentary: ;; Test suite for async-http-queue. Uses ERT with mocked `url-retrieve' ;; to test all functionality without network dependencies. ;; ;; To run all tests from the shell: ;; Emacs -Q --batch -L . -l async-http-queue-test.el -f ert-run-tests-batch-and-exit ;;; Code: (require 'ert) (require 'cl-lib) (require 'async-http-queue) ;;;; Test helpers (defvar async-http-queue-test--timeout 5 "Maximum seconds to wait for async operations in tests.") (defun async-http-queue-test--wait (predicate &optional timeout) "Wait until PREDICATE return non-nil or TIMEOUT seconds elapse. TIMEOUT defaults to `async-http-queue-test--timeout'." (let ((deadline (+ (float-time) (or timeout async-http-queue-test--timeout)))) (while (and (not (funcall predicate)) (< (float-time) deadline)) (accept-process-output nil 0.05)) (funcall predicate))) (defun async-http-queue-test--make-http-response (status-code body &optional content-type) "Build a fake HTTP response string with STATUS-CODE and BODY. CONTENT-TYPE defaults to application/json." (let ((ct (or content-type "application/json"))) (format "HTTP/1.1 %d OK\r\nContent-Type: %s\r\n\r\n%s" status-code ct body))) (defun async-http-queue-test--mock-url-retrieve (responses) "Return a function that replace `url-retrieve'. RESPONSES is an alist of (URL . RESPONSE-STRING) or (URL . :timeout) or (URL . :error). When a URL is fetched, the corresponding response is delivered asynchronously via a timer." (lambda (url callback &optional _cbargs _silent) (let* ((entry (assoc url responses)) (response (cdr entry)) (buf (generate-new-buffer (format " *test-url-%s*" url)))) (with-current-buffer buf (cond ((eq response :timeout) ;; Do not call the callback, simulating a hang nil) ((eq response :error) ;; Call callback with error status after a short delay (run-at-time 0.01 nil (lambda () (with-current-buffer buf (funcall callback (list :error '(error connection-refused))))))) (t ;; Insert the response and call the callback after a short delay (insert (or response (async-http-queue-test--make-http-response 200 "{}"))) (run-at-time 0.01 nil (lambda () (with-current-buffer buf (funcall callback nil))))))) buf))) (defmacro async-http-queue-test--with-mock (responses &rest body) "Execute BODY with `url-retrieve' replaced by a mock using RESPONSES." (declare (indent 1)) `(cl-letf (((symbol-function 'url-retrieve) (async-http-queue-test--mock-url-retrieve ,responses))) ,@body)) (defun async-http-queue-test--make-state (&optional urls) "Create a test state with URLS (defaults to 3 test URLs)." (let ((url-list (or urls '("http://test/1" "http://test/2" "http://test/3")))) (async-http-queue--state-create :queue (mapcar (lambda (url) `((url . ,url) (status . pending) (data . nil))) url-list) :active-workers 0 :max-concurrent 5 :timeout 10 :parser #'json-parse-buffer :completion-callback nil :error-callback nil))) ;;;; Struct tests (ert-deftest async-http-queue-test-state-create () "Verify struct creation with all fields." (let ((state (async-http-queue--state-create :queue '(((url . "http://a") (status . pending) (data . nil))) :active-workers 0 :max-concurrent 3 :timeout 15 :parser #'json-parse-buffer :completion-callback #'ignore :error-callback #'ignore))) (should (= (async-http-queue--state-active-workers state) 0)) (should (= (async-http-queue--state-max-concurrent state) 3)) (should (= (async-http-queue--state-timeout state) 15)) (should (eq (async-http-queue--state-parser state) #'json-parse-buffer)) (should (eq (async-http-queue--state-completion-callback state) #'ignore)) (should (eq (async-http-queue--state-error-callback state) #'ignore)) (should (= (length (async-http-queue--state-queue state)) 1)))) (ert-deftest async-http-queue-test-state-no-copier () "Verify copier is suppressed." (should-not (fboundp 'copy-async-http-queue--state))) ;;;; Update status tests (ert-deftest async-http-queue-test-update-status () "Verify status update for a matching URL." (let ((state (async-http-queue-test--make-state))) (async-http-queue--update-status state "http://test/2" 'processing) (let ((item (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/2")) (async-http-queue--state-queue state)))) (should (eq (alist-get 'status item) 'processing))))) (ert-deftest async-http-queue-test-update-status-no-side-effects () "Verify status update does not affect other items." (let ((state (async-http-queue-test--make-state))) (async-http-queue--update-status state "http://test/2" 'processing) (let ((item1 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/1")) (async-http-queue--state-queue state))) (item3 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/3")) (async-http-queue--state-queue state)))) (should (eq (alist-get 'status item1) 'pending)) (should (eq (alist-get 'status item3) 'pending))))) (ert-deftest async-http-queue-test-update-status-nonexistent-url () "Verify update with non-matching URL does not change anything." (let* ((state (async-http-queue-test--make-state)) (queue-before (copy-tree (async-http-queue--state-queue state)))) (async-http-queue--update-status state "http://nonexistent" 'done) (should (equal (mapcar (lambda (i) (alist-get 'status i)) (async-http-queue--state-queue state)) (mapcar (lambda (i) (alist-get 'status i)) queue-before))))) ;;;; Update data tests (ert-deftest async-http-queue-test-update-data () "Verify data update for a matching URL." (let ((state (async-http-queue-test--make-state)) (test-data '((title . "Hello")))) (async-http-queue--update-data state "http://test/1" test-data) (let ((item (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/1")) (async-http-queue--state-queue state)))) (should (equal (alist-get 'data item) test-data))))) (ert-deftest async-http-queue-test-update-data-no-side-effects () "Verify data update does not affect other items." (let ((state (async-http-queue-test--make-state))) (async-http-queue--update-data state "http://test/1" "some-data") (let ((item2 (seq-find (lambda (i) (equal (alist-get 'url i) "http://test/2")) (async-http-queue--state-queue state)))) (should (null (alist-get 'data item2)))))) ;;;; Check completion tests (ert-deftest async-http-queue-test-check-completion-all-done () "Verify completion fires when all items are done." (let* ((result nil) (state (async-http-queue-test--make-state '("http://a")))) (setf (async-http-queue--state-completion-callback state) (lambda (r) (setq result r))) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--update-data state "http://a" '((key . "val"))) (async-http-queue--check-completion state) (should (vectorp result)) (should (= (length result) 1)) (should (equal (aref result 0) '((key . "val")))))) (ert-deftest async-http-queue-test-check-completion-not-ready () "Verify completion does not fire while items are pending." (let* ((called nil) (state (async-http-queue-test--make-state '("http://a" "http://b")))) (setf (async-http-queue--state-completion-callback state) (lambda (_) (setq called t))) (async-http-queue--update-status state "http://a" 'done) ;; http://b is still pending (async-http-queue--check-completion state) (should-not called))) (ert-deftest async-http-queue-test-check-completion-with-errors () "Verify completion fires with nil for failed items." (let* ((result nil) (state (async-http-queue-test--make-state '("http://a" "http://b")))) (setf (async-http-queue--state-completion-callback state) (lambda (r) (setq result r))) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--update-data state "http://a" "ok") (async-http-queue--update-status state "http://b" 'error) (async-http-queue--check-completion state) (should (vectorp result)) (should (equal (aref result 0) "ok")) (should (null (aref result 1))))) (ert-deftest async-http-queue-test-check-completion-nil-callback () "Verify no error when completion-callback is nil." (let ((state (async-http-queue-test--make-state '("http://a")))) (setf (async-http-queue--state-completion-callback state) nil) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--check-completion state))) (ert-deftest async-http-queue-test-result-order-preserved () "Verify results vector preserves original URL order." (let* ((result nil) (urls '("http://a" "http://b" "http://c")) (state (async-http-queue-test--make-state urls))) (setf (async-http-queue--state-completion-callback state) (lambda (r) (setq result r))) ;; Complete in reverse order (async-http-queue--update-status state "http://c" 'done) (async-http-queue--update-data state "http://c" "third") (async-http-queue--update-status state "http://a" 'done) (async-http-queue--update-data state "http://a" "first") (async-http-queue--update-status state "http://b" 'done) (async-http-queue--update-data state "http://b" "second") (async-http-queue--check-completion state) (should (equal (aref result 0) "first")) (should (equal (aref result 1) "second")) (should (equal (aref result 2) "third")))) ;;;; Empty URL list tests (ert-deftest async-http-queue-test-empty-urls-with-callback () "Verify empty URL list calls callback with empty vector." (let ((result nil)) (async-http-queue nil :callback (lambda (r) (setq result r))) (should (vectorp result)) (should (= (length result) 0)))) (ert-deftest async-http-queue-test-empty-urls-no-callback () "Verify empty URL list with no callback does not error." (async-http-queue nil)) ;;;; Message tests (ert-deftest async-http-queue-test-message-singular () "Verify singular message for 1 URL." (let ((messages nil)) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages))) ((symbol-function 'url-retrieve) (lambda (&rest _) (generate-new-buffer " *test*")))) (async-http-queue '("http://test/1") :callback #'ignore) (should (member "Fetching 1 URL..." messages))))) (ert-deftest async-http-queue-test-message-plural () "Verify plural message for multiple URLs." (let ((messages nil)) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages))) ((symbol-function 'url-retrieve) (lambda (&rest _) (generate-new-buffer " *test*")))) (async-http-queue '("http://test/1" "http://test/2") :callback #'ignore) (should (member "Fetching 2 URLs..." messages))))) (ert-deftest async-http-queue-test-final-message-no-failures () "Verify final message when all succeed." (let* ((messages nil) (state (async-http-queue-test--make-state '("http://a")))) (setf (async-http-queue--state-completion-callback state) #'ignore) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--check-completion state) (should (member "Loaded 1 URLs" messages))))) (ert-deftest async-http-queue-test-final-message-with-failures () "Verify final message includes failure count." (let* ((messages nil) (state (async-http-queue-test--make-state '("http://a" "http://b")))) (setf (async-http-queue--state-completion-callback state) #'ignore) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--update-status state "http://b" 'error) (async-http-queue--check-completion state) (should (member "Loaded 1 URLs (1 failed)" messages))))) (ert-deftest async-http-queue-test-progress-message-large-batch () "Verify progress messages appear for batches > 10." (let* ((messages nil) (urls (cl-loop for i from 1 to 12 collect (format "http://test/%d" i))) (state (async-http-queue-test--make-state urls))) (setf (async-http-queue--state-completion-callback state) #'ignore) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) ;; Complete only some items so in-progress > 0 (async-http-queue--update-status state "http://test/1" 'done) (async-http-queue--check-completion state) ;; Should have a progress message (should (cl-some (lambda (m) (string-match "Loading URLs\\.\\.\\." m)) messages))))) (ert-deftest async-http-queue-test-no-progress-message-small-batch () "Verify no progress messages for batches <= 10." (let* ((messages nil) (state (async-http-queue-test--make-state '("http://a" "http://b")))) (cl-letf (((symbol-function 'message) (lambda (fmt &rest args) (push (apply #'format fmt args) messages)))) (async-http-queue--update-status state "http://a" 'done) (async-http-queue--check-completion state) (should-not (cl-some (lambda (m) (string-match "Loading URLs" m)) messages))))) ;;;; Default parameter tests (ert-deftest async-http-queue-test-default-max-concurrent () "Verify default max-concurrent is 5." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1")) (should (= (async-http-queue--state-max-concurrent captured-state) 5))))) (ert-deftest async-http-queue-test-default-timeout () "Verify default timeout is 10." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1")) (should (= (async-http-queue--state-timeout captured-state) 10))))) (ert-deftest async-http-queue-test-default-parser () "Verify default parser is `json-parse-buffer'." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1")) (should (eq (async-http-queue--state-parser captured-state) #'json-parse-buffer))))) (ert-deftest async-http-queue-test-custom-max-concurrent () "Verify custom max-concurrent is stored." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1") :max-concurrent 3) (should (= (async-http-queue--state-max-concurrent captured-state) 3))))) (ert-deftest async-http-queue-test-custom-timeout () "Verify custom timeout is stored." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1") :timeout 30) (should (= (async-http-queue--state-timeout captured-state) 30))))) (ert-deftest async-http-queue-test-nil-parser () "Verify nil parser is stored for raw text mode." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1") :parser nil) (should (null (async-http-queue--state-parser captured-state)))))) (ert-deftest async-http-queue-test-custom-parser () "Verify custom parser function is stored." (let (captured-state (my-parser (lambda () "custom"))) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://test/1") :parser my-parser) (should (eq (async-http-queue--state-parser captured-state) my-parser))))) ;;;; Queue construction tests (ert-deftest async-http-queue-test-queue-construction () "Verify queue items have correct shape." (let (captured-state) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://a" "http://b")) (let ((queue (async-http-queue--state-queue captured-state))) (should (= (length queue) 2)) (dolist (item queue) (should (assoc 'url item)) (should (assoc 'status item)) (should (assoc 'data item)) (should (eq (alist-get 'status item) 'pending)) (should (null (alist-get 'data item)))) (should (equal (alist-get 'url (nth 0 queue)) "http://a")) (should (equal (alist-get 'url (nth 1 queue)) "http://b")))))) (ert-deftest async-http-queue-test-callbacks-stored () "Verify callbacks are stored in state." (let (captured-state (my-cb (lambda (_) nil)) (my-err-cb (lambda (_) nil))) (cl-letf (((symbol-function 'async-http-queue--process) (lambda (state) (setq captured-state state))) ((symbol-function 'message) #'ignore)) (async-http-queue '("http://a") :callback my-cb :error-callback my-err-cb) (should (eq (async-http-queue--state-completion-callback captured-state) my-cb)) (should (eq (async-http-queue--state-error-callback captured-state) my-err-cb))))) ;;;; Process-next-pending tests (ert-deftest async-http-queue-test-process-next-respects-limit () "Verify no new worker starts when at capacity." (let* ((fetch-called nil) (state (async-http-queue-test--make-state '("http://a")))) (setf (async-http-queue--state-active-workers state) 5) (setf (async-http-queue--state-max-concurrent state) 5) (cl-letf (((symbol-function 'async-http-queue--fetch-url) (lambda (&rest _) (setq fetch-called t)))) (async-http-queue--process-next-pending state) (should-not fetch-called)))) (ert-deftest async-http-queue-test-process-next-starts-worker () "Verify a worker starts when below capacity." (let* ((fetch-called nil) (fetched-url nil) (state (async-http-queue-test--make-state '("http://a")))) (setf (async-http-queue--state-active-workers state) 0) (setf (async-http-queue--state-max-concurrent state) 5) (cl-letf (((symbol-function 'async-http-queue--fetch-url) (lambda (_state url _success _error) (setq fetch-called t fetched-url url)))) (async-http-queue--process-next-pending state) (should fetch-called) (should (equal fetched-url "http://a")) (should (= (async-http-queue--state-active-workers state) 1))))) (ert-deftest async-http-queue-test-process-next-skips-non-pending () "Verify already-processing items are skipped." (let* ((fetch-called nil) (state (async-http-queue-test--make-state '("http://a" "http://b")))) (async-http-queue--update-status state "http://a" 'processing) (setf (async-http-queue--state-active-workers state) 1) (cl-letf (((symbol-function 'async-http-queue--fetch-url) (lambda (_state url _success _error) (setq fetch-called url)))) (async-http-queue--process-next-pending state) (should (equal fetch-called "http://b"))))) (ert-deftest async-http-queue-test-process-next-noop-no-pending () "Verify noop when no pending items remain." (let* ((fetch-called nil) (state (async-http-queue-test--make-state '("http://a")))) (async-http-queue--update-status state "http://a" 'done) (setf (async-http-queue--state-active-workers state) 0) (cl-letf (((symbol-function 'async-http-queue--fetch-url) (lambda (&rest _) (setq fetch-called t)))) (async-http-queue--process-next-pending state) (should-not fetch-called)))) ;;;; Status transition tests (ert-deftest async-http-queue-test-status-transitions () "Verify the valid status transitions: pending -> processing -> done/error." (let ((state (async-http-queue-test--make-state '("http://a" "http://b")))) ;; Start as pending (should (eq (alist-get 'status (nth 0 (async-http-queue--state-queue state))) 'pending)) ;; Transition to processing (async-http-queue--update-status state "http://a" 'processing) (should (eq (alist-get 'status (seq-find (lambda (i) (equal (alist-get 'url i) "http://a")) (async-http-queue--state-queue state))) 'processing)) ;; Transition to done (async-http-queue--update-status state "http://a" 'done) (should (eq (alist-get 'status (seq-find (lambda (i) (equal (alist-get 'url i) "http://a")) (async-http-queue--state-queue state))) 'done)) ;; Transition to error (async-http-queue--update-status state "http://b" 'processing) (async-http-queue--update-status state "http://b" 'error) (should (eq (alist-get 'status (seq-find (lambda (i) (equal (alist-get 'url i) "http://b")) (async-http-queue--state-queue state))) 'error)))) ;;;; Fetch URL tests (with mocked url-retrieve) (ert-deftest async-http-queue-test-fetch-url-success-json () "Verify successful fetch with JSON parser." (let* ((result nil) (error-called nil) (json-body "{\"title\": \"hello\"}") (response (async-http-queue-test--make-http-response 200 json-body)) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser #'json-parse-buffer :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should-not error-called) (should result) (should (equal (gethash "title" result) "hello"))))) (ert-deftest async-http-queue-test-fetch-url-success-raw-text () "Verify successful fetch with nil parser returns raw text." (let* ((result nil) (error-called nil) (body "Hello, world!") (response (async-http-queue-test--make-http-response 200 body "text/plain")) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should-not error-called) (should (equal result "Hello, world!"))))) (ert-deftest async-http-queue-test-fetch-url-success-custom-parser () "Verify successful fetch with custom parser." (let* ((result nil) (error-called nil) (body "some data") (response (async-http-queue-test--make-http-response 200 body "text/plain")) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser (lambda () (upcase (buffer-substring-no-properties (point) (point-max)))) :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should-not error-called) (should (equal result "SOME DATA"))))) (ert-deftest async-http-queue-test-fetch-url-http-error () "Verify non-2xx status code triggers error callback." (let* ((result nil) (error-called nil) (response (async-http-queue-test--make-http-response 404 "Not Found")) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should error-called) (should-not result)))) (ert-deftest async-http-queue-test-fetch-url-500-error () "Verify 500 status triggers error callback." (let* ((result nil) (error-called nil) (response (async-http-queue-test--make-http-response 500 "Internal Server Error")) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should error-called) (should-not result)))) (ert-deftest async-http-queue-test-fetch-url-connection-error () "Verify connection error triggers error callback." (let* ((result nil) (error-called nil) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock '(("http://test/1" . :error)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should error-called) (should-not result)))) (ert-deftest async-http-queue-test-fetch-url-parser-error () "Verify parser failure triggers error callback." (let* ((result nil) (error-called nil) (response (async-http-queue-test--make-http-response 200 "not valid json")) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser #'json-parse-buffer :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should error-called) (should-not result)))) (ert-deftest async-http-queue-test-fetch-url-invalid-response () "Verify invalid HTTP response (no status line) triggers error." (let* ((result nil) (error-called nil) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 10 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock '(("http://test/1" . "garbage response with no http status")) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () (or result error-called)))) (should error-called) (should-not result)))) (ert-deftest async-http-queue-test-fetch-url-timeout () "Verify timeout triggers error callback." (let* ((result nil) (error-called nil) (state (async-http-queue--state-create :queue nil :active-workers 0 :max-concurrent 5 :timeout 0.1 :parser nil :completion-callback nil :error-callback nil))) (async-http-queue-test--with-mock '(("http://test/1" . :timeout)) (async-http-queue--fetch-url state "http://test/1" (lambda (data) (setq result data)) (lambda () (setq error-called t))) (should (async-http-queue-test--wait (lambda () error-called) 3)) (should error-called) (should-not result)))) ;;;; Full integration tests (with mocked url-retrieve) (ert-deftest async-http-queue-test-integration-single-url () "Verify full flow with a single URL." (let* ((result nil) (json-body "{\"id\": 1}") (response (async-http-queue-test--make-http-response 200 json-body))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue '("http://test/1") :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (vectorp result)) (should (= (length result) 1)) (should (= (gethash "id" (aref result 0)) 1))))) (ert-deftest async-http-queue-test-integration-multiple-urls () "Verify full flow with multiple URLs." (let* ((result nil) (responses `(("http://test/1" . ,(async-http-queue-test--make-http-response 200 "{\"id\": 1}")) ("http://test/2" . ,(async-http-queue-test--make-http-response 200 "{\"id\": 2}")) ("http://test/3" . ,(async-http-queue-test--make-http-response 200 "{\"id\": 3}"))))) (async-http-queue-test--with-mock responses (async-http-queue '("http://test/1" "http://test/2" "http://test/3") :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (= (length result) 3)) (should (= (gethash "id" (aref result 0)) 1)) (should (= (gethash "id" (aref result 1)) 2)) (should (= (gethash "id" (aref result 2)) 3))))) (ert-deftest async-http-queue-test-integration-mixed-success-failure () "Verify results contain nil for failed URLs." (let* ((result nil) (error-urls nil) (responses `(("http://test/1" . ,(async-http-queue-test--make-http-response 200 "{\"id\": 1}")) ("http://test/2" . ,(async-http-queue-test--make-http-response 500 "error")) ("http://test/3" . ,(async-http-queue-test--make-http-response 200 "{\"id\": 3}"))))) (async-http-queue-test--with-mock responses (async-http-queue '("http://test/1" "http://test/2" "http://test/3") :callback (lambda (r) (setq result r)) :error-callback (lambda (url) (push url error-urls))) (should (async-http-queue-test--wait (lambda () result))) (should (= (length result) 3)) (should (= (gethash "id" (aref result 0)) 1)) (should (null (aref result 1))) (should (= (gethash "id" (aref result 2)) 3)) (should (member "http://test/2" error-urls))))) (ert-deftest async-http-queue-test-integration-all-fail () "Verify all-failure scenario." (let* ((result nil) (error-urls nil) (responses '(("http://test/1" . :error) ("http://test/2" . :error)))) (async-http-queue-test--with-mock responses (async-http-queue '("http://test/1" "http://test/2") :callback (lambda (r) (setq result r)) :error-callback (lambda (url) (push url error-urls))) (should (async-http-queue-test--wait (lambda () result))) (should (= (length result) 2)) (should (null (aref result 0))) (should (null (aref result 1))) (should (= (length error-urls) 2))))) (ert-deftest async-http-queue-test-integration-raw-text () "Verify raw text mode with nil parser." (let* ((result nil) (body "Hello raw text") (response (async-http-queue-test--make-http-response 200 body "text/plain"))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue '("http://test/1") :parser nil :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (equal (aref result 0) "Hello raw text"))))) (ert-deftest async-http-queue-test-integration-custom-parser () "Verify custom parser in full flow." (let* ((result nil) (body "data") (response (async-http-queue-test--make-http-response 200 body "text/xml"))) (async-http-queue-test--with-mock `(("http://test/1" . ,response)) (async-http-queue '("http://test/1") :parser (lambda () (upcase (buffer-substring-no-properties (point) (point-max)))) :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (equal (aref result 0) "DATA"))))) (ert-deftest async-http-queue-test-integration-custom-concurrency () "Verify custom concurrency is passed through." (let* ((result nil) (max-seen-workers 0) (response (async-http-queue-test--make-http-response 200 "\"ok\""))) (cl-letf (((symbol-function 'url-retrieve) (lambda (_url callback &optional _cbargs _silent) (let ((buf (generate-new-buffer " *test*"))) (with-current-buffer buf (insert response)) ;; Delay to allow concurrency measurement (run-at-time 0.05 nil (lambda () (with-current-buffer buf (funcall callback nil)))) buf))) ((symbol-function 'async-http-queue--process-next-pending) (let ((orig (symbol-function 'async-http-queue--process-next-pending))) (lambda (state) (funcall orig state) (setq max-seen-workers (max max-seen-workers (async-http-queue--state-active-workers state))))))) (async-http-queue '("http://test/1" "http://test/2" "http://test/3" "http://test/4" "http://test/5") :max-concurrent 2 :parser nil :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (<= max-seen-workers 2))))) (ert-deftest async-http-queue-test-integration-error-callback-called () "Verify error-callback receives the failed URL." (let* ((result nil) (failed-url nil)) (async-http-queue-test--with-mock '(("http://test/1" . :error)) (async-http-queue '("http://test/1") :callback (lambda (r) (setq result r)) :error-callback (lambda (url) (setq failed-url url))) (should (async-http-queue-test--wait (lambda () result))) (should (equal failed-url "http://test/1"))))) (ert-deftest async-http-queue-test-integration-no-error-callback () "Verify no crash when error-callback is nil and errors occur." (let ((result nil)) (async-http-queue-test--with-mock '(("http://test/1" . :error)) (async-http-queue '("http://test/1") :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (null (aref result 0)))))) (ert-deftest async-http-queue-test-integration-timeout () "Verify timeout in full flow." (let* ((result nil) (error-urls nil)) (async-http-queue-test--with-mock '(("http://test/1" . :timeout)) (async-http-queue '("http://test/1") :timeout 0.2 :callback (lambda (r) (setq result r)) :error-callback (lambda (url) (push url error-urls))) (should (async-http-queue-test--wait (lambda () result) 5)) (should (null (aref result 0))) (should (member "http://test/1" error-urls))))) (ert-deftest async-http-queue-test-integration-result-is-vector () "Verify callback receives a vector, not a list." (let ((result nil)) (async-http-queue-test--with-mock `(("http://test/1" . ,(async-http-queue-test--make-http-response 200 "\"data\""))) (async-http-queue '("http://test/1") :callback (lambda (r) (setq result r))) (should (async-http-queue-test--wait (lambda () result))) (should (vectorp result))))) ;;;; Provide test feature (ert-deftest async-http-queue-test-provide () "Verify the package provides the expected feature." (should (featurep 'async-http-queue))) (provide 'async-http-queue-test) ;;; async-http-queue-test.el ends here