diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-28 18:10:17 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-28 19:09:22 -0500 |
| commit | dc0db0f0e12d8af6d1d54a5dde1cd16cf890a33d (patch) | |
| tree | 9d174e136d62981c1bded06fe423d08bddc91e04 /tests | |
| parent | 3a846506399dc12ab219bfa8047947c122dd1d04 (diff) | |
| download | gloss-dc0db0f0e12d8af6d1d54a5dde1cd16cf890a33d.tar.gz gloss-dc0db0f0e12d8af6d1d54a5dde1cd16cf890a33d.zip | |
test: add gloss-fetch test suite (red phase)
Eight test files cover the network layer's public and internal contract.
The boundary mock is `url-retrieve-synchronously', wrapped by a small
`testutil-gloss-fetch' helper that builds response buffers in the shape
the url library returns.
Tests cover the 200 happy paths (anaphora and SBIR fixtures), 404 to
:no-defs, 5xx and 4xx-other and malformed JSON to :server-error, 429 to
:rate-limited, nil-from-url to :unreachable, the libxml availability
probe (one-shot, signals user-error when absent), the registry walker
ordering, and the pure HTML strip helper across N/B/E.
Tests fail on missing `gloss-fetch--*' functions, as expected for red
phase.
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/test-gloss-fetch--definitions-200-returns-ok.el | 62 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--definitions-404-returns-no-defs.el | 48 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--definitions-429-returns-rate-limited.el | 43 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--definitions-500-returns-server-error.el | 55 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--definitions-timeout-returns-unreachable.el | 44 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--libxml-probe.el | 62 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--multi-source-walks-registry.el | 88 | ||||
| -rw-r--r-- | tests/test-gloss-fetch--strip-html.el | 59 | ||||
| -rw-r--r-- | tests/testutil-gloss-fetch.el | 48 |
9 files changed, 509 insertions, 0 deletions
diff --git a/tests/test-gloss-fetch--definitions-200-returns-ok.el b/tests/test-gloss-fetch--definitions-200-returns-ok.el new file mode 100644 index 0000000..fee997b --- /dev/null +++ b/tests/test-gloss-fetch--definitions-200-returns-ok.el @@ -0,0 +1,62 @@ +;;; test-gloss-fetch--definitions-200-returns-ok.el --- 200 path tests -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; Normal/Boundary cases: a 200 response with valid JSON returns +;; (:ok DEFS) and each def is a plist with :source and :text. Uses the +;; captured Wiktionary fixtures replayed through a mocked +;; `url-retrieve-synchronously'. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-definitions-200-anaphora-returns-ok () + "Normal: anaphora fixture (single English sense) returns (:ok DEFS)." + (let ((body (gloss-test--load-wiktionary-fixture "anaphora"))) + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response body)) + (let* ((result (gloss-fetch-definitions "anaphora")) + (defs (plist-get result :ok))) + (should (eq (car result) :ok)) + (should (consp defs)) + (should (>= (length defs) 1)) + (let ((first (car defs))) + (should (eq (plist-get first :source) 'wiktionary)) + (should (stringp (plist-get first :text))) + (should (> (length (plist-get first :text)) 0)) + ;; HTML stripped — no angle brackets in the text. + (should-not (string-match-p "<" (plist-get first :text)))))))) + +(ert-deftest test-gloss-fetch-definitions-200-sbir-returns-multiple-senses () + "Boundary: SBIR fixture has multiple senses; all returned as separate plists." + (let ((body (gloss-test--load-wiktionary-fixture "SBIR"))) + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response body)) + (let* ((result (gloss-fetch-definitions "SBIR")) + (defs (plist-get result :ok))) + (should (eq (car result) :ok)) + (should (>= (length defs) 1)) + (dolist (d defs) + (should (eq (plist-get d :source) 'wiktionary)) + (should (stringp (plist-get d :text)))))))) + +(ert-deftest test-gloss-fetch-definitions-200-encodes-spaces-in-url () + "Boundary: a multi-word term URL-encodes the space." + (let ((seen-url nil) + (body "{}")) + (gloss-fetch-test--with-mocked-url + (lambda (url) + (setq seen-url url) + (gloss-fetch-test--ok-response body)) + (gloss-fetch-definitions "hapax legomenon")) + (should seen-url) + (should (string-match-p "hapax%20legomenon\\|hapax_legomenon" seen-url)))) + +(provide 'test-gloss-fetch--definitions-200-returns-ok) +;;; test-gloss-fetch--definitions-200-returns-ok.el ends here diff --git a/tests/test-gloss-fetch--definitions-404-returns-no-defs.el b/tests/test-gloss-fetch--definitions-404-returns-no-defs.el new file mode 100644 index 0000000..d8cd257 --- /dev/null +++ b/tests/test-gloss-fetch--definitions-404-returns-no-defs.el @@ -0,0 +1,48 @@ +;;; test-gloss-fetch--definitions-404-returns-no-defs.el --- 404 path -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; HTTP 404 from a source maps to :no-defs. When every source returns +;; :no-defs, the user-facing rollup is :empty with the source listed +;; under :no-defs and nothing under :failed. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-definitions-404-rolls-up-to-empty-no-defs () + "Normal: a 404 from the only source rolls up to (:empty :no-defs (wiktionary) :failed nil)." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 404 Not Found" + "{\"detail\":\"Page not found\"}")) + (let ((result (gloss-fetch-definitions "asdf-not-a-word"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :no-defs))) + (should-not (plist-get result :failed))))) + +(ert-deftest test-gloss-fetch-definitions-200-empty-rolls-up-to-empty-no-defs () + "Boundary: a 200 with an empty JSON object also maps to :no-defs." + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response "{}")) + (let ((result (gloss-fetch-definitions "term"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :no-defs))) + (should-not (plist-get result :failed))))) + +(ert-deftest test-gloss-fetch-definitions-200-no-english-rolls-up-to-no-defs () + "Boundary: a 200 response with only non-English keys maps to :no-defs." + ;; v1 ignores everything but the en key per the design doc. + (let ((body "{\"fr\":[{\"partOfSpeech\":\"Noun\",\"language\":\"French\",\"definitions\":[{\"definition\":\"Un mot.\"}]}]}")) + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response body)) + (let ((result (gloss-fetch-definitions "term"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :no-defs))))))) + +(provide 'test-gloss-fetch--definitions-404-returns-no-defs) +;;; test-gloss-fetch--definitions-404-returns-no-defs.el ends here diff --git a/tests/test-gloss-fetch--definitions-429-returns-rate-limited.el b/tests/test-gloss-fetch--definitions-429-returns-rate-limited.el new file mode 100644 index 0000000..a5fb719 --- /dev/null +++ b/tests/test-gloss-fetch--definitions-429-returns-rate-limited.el @@ -0,0 +1,43 @@ +;;; test-gloss-fetch--definitions-429-returns-rate-limited.el --- 429 path -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; HTTP 429 is its own per-source status (`:rate-limited'), separated +;; from `:server-error' so the v2 user-facing wording can call it out +;; distinctly. At the rollup it joins :failed. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-definitions-429-rolls-up-to-failed () + "Normal: HTTP 429 maps the source to :rate-limited, which joins :failed." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 429 Too Many Requests" "")) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed))) + (should-not (plist-get result :no-defs))))) + +(ert-deftest test-gloss-fetch-definitions-429-tracked-separately-internally () + "Boundary: per-source status taxonomy distinguishes :rate-limited from :server-error. + +Verifies the internal walker exposes the per-source result so the +debug log can carry the right tag. Calls +`gloss-fetch--collect' (the internal entry point that returns the +per-source result list) and inspects the :status field." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 429 Too Many Requests" "")) + (let* ((per-source (gloss-fetch--collect "anaphora")) + (entry (car per-source))) + (should (eq (plist-get entry :source) 'wiktionary)) + (should (eq (plist-get entry :status) :rate-limited))))) + +(provide 'test-gloss-fetch--definitions-429-returns-rate-limited) +;;; test-gloss-fetch--definitions-429-returns-rate-limited.el ends here diff --git a/tests/test-gloss-fetch--definitions-500-returns-server-error.el b/tests/test-gloss-fetch--definitions-500-returns-server-error.el new file mode 100644 index 0000000..e81efe2 --- /dev/null +++ b/tests/test-gloss-fetch--definitions-500-returns-server-error.el @@ -0,0 +1,55 @@ +;;; test-gloss-fetch--definitions-500-returns-server-error.el --- 5xx path -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; HTTP 5xx, malformed JSON, schema mismatch, and 4xx other than 404/429 +;; all roll up to :server-error. When every source fails this way, the +;; user-facing rollup is :empty with the source listed under :failed. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-definitions-500-rolls-up-to-failed () + "Normal: HTTP 500 maps the source to :server-error (in :failed)." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 500 Internal Server Error" + "Server is sad.")) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed))) + (should-not (plist-get result :no-defs))))) + +(ert-deftest test-gloss-fetch-definitions-503-rolls-up-to-failed () + "Normal: HTTP 503 maps the source to :server-error (in :failed)." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 503 Service Unavailable" "")) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed)))))) + +(ert-deftest test-gloss-fetch-definitions-malformed-json-rolls-up-to-failed () + "Boundary: a 200 with non-JSON body also maps to :server-error." + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response "<html>not json</html>")) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed)))))) + +(ert-deftest test-gloss-fetch-definitions-400-rolls-up-to-failed () + "Error: HTTP 400 (4xx other than 404/429) maps to :server-error (in :failed)." + (gloss-fetch-test--with-mocked-url + (lambda (_url) + (gloss-fetch-test--status-response "HTTP/1.1 400 Bad Request" "")) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed)))))) + +(provide 'test-gloss-fetch--definitions-500-returns-server-error) +;;; test-gloss-fetch--definitions-500-returns-server-error.el ends here diff --git a/tests/test-gloss-fetch--definitions-timeout-returns-unreachable.el b/tests/test-gloss-fetch--definitions-timeout-returns-unreachable.el new file mode 100644 index 0000000..881b783 --- /dev/null +++ b/tests/test-gloss-fetch--definitions-timeout-returns-unreachable.el @@ -0,0 +1,44 @@ +;;; test-gloss-fetch--definitions-timeout-returns-unreachable.el --- timeout path -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; A timeout (or any other transport-level failure) makes +;; `url-retrieve-synchronously' return nil; the source maps to +;; :unreachable, which joins :failed at the rollup. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-definitions-timeout-rolls-up-to-failed () + "Normal: nil from url-retrieve-synchronously rolls up to :failed." + (gloss-fetch-test--with-mocked-url + (lambda (_url) nil) + (let ((result (gloss-fetch-definitions "anaphora"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :failed))) + (should-not (plist-get result :no-defs))))) + +(ert-deftest test-gloss-fetch-definitions-timeout-marks-source-unreachable () + "Boundary: per-source status is :unreachable, distinct from :server-error." + (gloss-fetch-test--with-mocked-url + (lambda (_url) nil) + (let* ((per-source (gloss-fetch--collect "anaphora")) + (entry (car per-source))) + (should (eq (plist-get entry :source) 'wiktionary)) + (should (eq (plist-get entry :status) :unreachable))))) + +(ert-deftest test-gloss-fetch-definitions-error-signal-marks-source-unreachable () + "Error: a signaled error inside url-retrieve-synchronously also yields :unreachable." + (gloss-fetch-test--with-mocked-url + (lambda (_url) (error "Connection refused")) + (let* ((per-source (gloss-fetch--collect "anaphora")) + (entry (car per-source))) + (should (eq (plist-get entry :status) :unreachable))))) + +(provide 'test-gloss-fetch--definitions-timeout-returns-unreachable) +;;; test-gloss-fetch--definitions-timeout-returns-unreachable.el ends here diff --git a/tests/test-gloss-fetch--libxml-probe.el b/tests/test-gloss-fetch--libxml-probe.el new file mode 100644 index 0000000..830a278 --- /dev/null +++ b/tests/test-gloss-fetch--libxml-probe.el @@ -0,0 +1,62 @@ +;;; test-gloss-fetch--libxml-probe.el --- libxml availability probe tests -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; libxml is a precondition for online fetch. First call probes once; +;; absent libxml triggers a one-shot `user-error' and disables online +;; fetch package-wide for the session. Subsequent attempts in the same +;; session also signal `user-error'. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) +(require 'testutil-gloss-fetch) + +(ert-deftest test-gloss-fetch-libxml-absent-signals-user-error () + "Error: with libxml unavailable, the first fetch signals user-error and disables online." + (let ((gloss-fetch--libxml-disabled nil)) + (cl-letf (((symbol-function 'gloss-fetch--libxml-available-p) + (lambda () nil))) + (should-error (gloss-fetch-definitions "anything") :type 'user-error) + ;; Subsequent attempts also signal — no auto-recovery in the same session. + (should-error (gloss-fetch-definitions "again") :type 'user-error)))) + +(ert-deftest test-gloss-fetch-libxml-error-mentions-libxml2 () + "Error: the user-error message names libxml2 so the user can act." + (let ((gloss-fetch--libxml-disabled nil)) + (cl-letf (((symbol-function 'gloss-fetch--libxml-available-p) + (lambda () nil))) + (let ((err (should-error (gloss-fetch-definitions "x") :type 'user-error))) + (should (string-match-p "libxml2" (error-message-string err))))))) + +(ert-deftest test-gloss-fetch-libxml-probe-runs-only-once () + "Boundary: the libxml availability probe is invoked at most once per session." + (let ((probe-calls 0) + (gloss-fetch--libxml-disabled nil) + (gloss-fetch--libxml-checked nil)) + (cl-letf (((symbol-function 'gloss-fetch--libxml-available-p) + (lambda () (cl-incf probe-calls) t))) + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response "{}")) + (gloss-fetch-definitions "first") + (gloss-fetch-definitions "second") + (gloss-fetch-definitions "third")) + (should (= 1 probe-calls))))) + +(ert-deftest test-gloss-fetch-libxml-present-allows-fetch () + "Normal: when libxml is available, fetch proceeds normally." + (let ((gloss-fetch--libxml-disabled nil) + (gloss-fetch--libxml-checked nil)) + (cl-letf (((symbol-function 'gloss-fetch--libxml-available-p) + (lambda () t))) + (gloss-fetch-test--with-mocked-url + (lambda (_url) (gloss-fetch-test--ok-response "{}")) + (let ((result (gloss-fetch-definitions "term"))) + (should (eq (car result) :empty)) + (should (member 'wiktionary (plist-get result :no-defs)))))))) + +(provide 'test-gloss-fetch--libxml-probe) +;;; test-gloss-fetch--libxml-probe.el ends here diff --git a/tests/test-gloss-fetch--multi-source-walks-registry.el b/tests/test-gloss-fetch--multi-source-walks-registry.el new file mode 100644 index 0000000..fe04e02 --- /dev/null +++ b/tests/test-gloss-fetch--multi-source-walks-registry.el @@ -0,0 +1,88 @@ +;;; test-gloss-fetch--multi-source-walks-registry.el --- Registry walk tests -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; `gloss-fetch--sources' is an alist (source-symbol → fetcher fn). +;; `gloss-fetch-sources' is the user-facing defcustom that orders the +;; walk. Each fetcher returns a per-source result plist. The walker +;; aggregates results across all configured sources. + +;;; Code: + +(require 'ert) +(require 'cl-lib) +(require 'gloss-fetch) + +(ert-deftest test-gloss-fetch-collect-walks-registry-in-defcustom-order () + "Normal: walker visits each source in `gloss-fetch-sources' order, gathers a result per source." + (let* ((calls nil) + (gloss-fetch--sources + `((alpha . ,(lambda (term) + (push 'alpha calls) + (list :source 'alpha :status :ok :defs + (list (list :source 'alpha :text (concat "A:" term)))))) + (beta . ,(lambda (term) + (push 'beta calls) + (list :source 'beta :status :no-defs + :reason (concat "no entry for " term)))))) + (gloss-fetch-sources '(alpha beta)) + (per-source (gloss-fetch--collect "x"))) + (should (equal (reverse calls) '(alpha beta))) + (should (= 2 (length per-source))) + (should (eq 'alpha (plist-get (nth 0 per-source) :source))) + (should (eq 'beta (plist-get (nth 1 per-source) :source))))) + +(ert-deftest test-gloss-fetch-rollup-mixes-no-defs-and-failed () + "Boundary: a mix of :no-defs and :server-error rolls up with both lists populated." + (let* ((gloss-fetch--sources + `((alpha . ,(lambda (_term) + (list :source 'alpha :status :no-defs :reason "404"))) + (beta . ,(lambda (_term) + (list :source 'beta :status :server-error :reason "HTTP 503"))))) + (gloss-fetch-sources '(alpha beta)) + (result (gloss-fetch-definitions "x"))) + (should (eq (car result) :empty)) + (should (member 'alpha (plist-get result :no-defs))) + (should (member 'beta (plist-get result :failed))))) + +(ert-deftest test-gloss-fetch-rollup-any-ok-yields-ok () + "Boundary: if any source returns :ok with defs, the rollup is (:ok DEFS)." + (let* ((gloss-fetch--sources + `((alpha . ,(lambda (_term) + (list :source 'alpha :status :no-defs :reason "404"))) + (beta . ,(lambda (_term) + (list :source 'beta :status :ok :defs + (list (list :source 'beta :text "got it"))))))) + (gloss-fetch-sources '(alpha beta)) + (result (gloss-fetch-definitions "x"))) + (should (eq (car result) :ok)) + (should (= 1 (length (plist-get result :ok)))) + (should (equal "got it" (plist-get (car (plist-get result :ok)) :text))))) + +(ert-deftest test-gloss-fetch-collect-skips-source-not-in-defcustom () + "Error: a source registered in `gloss-fetch--sources' but not in `gloss-fetch-sources' is skipped." + (let* ((called nil) + (gloss-fetch--sources + `((alpha . ,(lambda (_term) + (setq called t) + (list :source 'alpha :status :ok :defs + (list (list :source 'alpha :text "x"))))))) + (gloss-fetch-sources '()) ; empty — skip everything + (per-source (gloss-fetch--collect "x"))) + (should-not called) + (should (null per-source)))) + +(ert-deftest test-gloss-fetch-collect-skips-unknown-source-symbol () + "Error: a symbol in `gloss-fetch-sources' with no registered fetcher is silently skipped." + (let* ((gloss-fetch--sources + `((alpha . ,(lambda (_term) + (list :source 'alpha :status :ok :defs + (list (list :source 'alpha :text "x"))))))) + (gloss-fetch-sources '(zeta alpha)) + (per-source (gloss-fetch--collect "x"))) + (should (= 1 (length per-source))) + (should (eq 'alpha (plist-get (car per-source) :source))))) + +(provide 'test-gloss-fetch--multi-source-walks-registry) +;;; test-gloss-fetch--multi-source-walks-registry.el ends here diff --git a/tests/test-gloss-fetch--strip-html.el b/tests/test-gloss-fetch--strip-html.el new file mode 100644 index 0000000..61f7e70 --- /dev/null +++ b/tests/test-gloss-fetch--strip-html.el @@ -0,0 +1,59 @@ +;;; test-gloss-fetch--strip-html.el --- HTML strip helper tests -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: +;; `gloss-fetch--strip-html' converts a fragment of HTML to plain text +;; using `libxml-parse-html-region' (no italic/bold preservation). +;; Returns nil when libxml fails on the fragment so the caller can drop +;; that sense. + +;;; Code: + +(require 'ert) +(require 'gloss-fetch) + +(ert-deftest test-gloss-fetch-strip-html-plain-text-roundtrips () + "Normal: a string with no markup comes back unchanged (modulo whitespace)." + (skip-unless (fboundp 'libxml-parse-html-region)) + (should (equal (gloss-fetch--strip-html "Just plain text.") + "Just plain text."))) + +(ert-deftest test-gloss-fetch-strip-html-removes-tags () + "Normal: anchor and span tags are stripped, leaving inner text." + (skip-unless (fboundp 'libxml-parse-html-region)) + (let ((stripped (gloss-fetch--strip-html + "The <a href=\"/foo\">repetition</a> of a <i>phrase</i>."))) + (should (string-match-p "repetition" stripped)) + (should (string-match-p "phrase" stripped)) + (should-not (string-match-p "<" stripped)) + (should-not (string-match-p "href" stripped)))) + +(ert-deftest test-gloss-fetch-strip-html-empty-string-returns-empty () + "Boundary: empty input returns an empty string (or nil)." + (skip-unless (fboundp 'libxml-parse-html-region)) + (let ((result (gloss-fetch--strip-html ""))) + (should (or (null result) (equal result ""))))) + +(ert-deftest test-gloss-fetch-strip-html-entities-decoded () + "Boundary: HTML entities decode to their characters." + (skip-unless (fboundp 'libxml-parse-html-region)) + (let ((stripped (gloss-fetch--strip-html "Smith & Wesson"))) + (should (string-match-p "&" stripped)) + (should-not (string-match-p "&" stripped)))) + +(ert-deftest test-gloss-fetch-strip-html-collapses-whitespace () + "Boundary: runs of internal whitespace collapse to single spaces; result is trimmed." + (skip-unless (fboundp 'libxml-parse-html-region)) + (let ((stripped (gloss-fetch--strip-html " hello <b>world</b> "))) + (should (equal stripped "hello world")))) + +(ert-deftest test-gloss-fetch-strip-html-failure-returns-nil () + "Error: when libxml-parse-html-region raises, return nil so the caller can drop the sense." + (skip-unless (fboundp 'libxml-parse-html-region)) + (cl-letf (((symbol-function 'libxml-parse-html-region) + (lambda (&rest _) (error "libxml exploded")))) + (should-not (gloss-fetch--strip-html "any input")))) + +(provide 'test-gloss-fetch--strip-html) +;;; test-gloss-fetch--strip-html.el ends here diff --git a/tests/testutil-gloss-fetch.el b/tests/testutil-gloss-fetch.el new file mode 100644 index 0000000..a144d91 --- /dev/null +++ b/tests/testutil-gloss-fetch.el @@ -0,0 +1,48 @@ +;;; testutil-gloss-fetch.el --- Test helpers for gloss-fetch -*- lexical-binding: t -*- + +;; SPDX-License-Identifier: GPL-3.0-or-later + +;;; Commentary: + +;; Helpers for tests that exercise `gloss-fetch'. The boundary mock is +;; `url-retrieve-synchronously', so these helpers build response buffers +;; in the shape Emacs's url library returns: status line, blank line, +;; body. The body comes from a JSON fixture loaded via +;; `gloss-test--load-wiktionary-fixture' (provided by testutil-gloss.el +;; on a parallel branch). + +;;; Code: + +(defun gloss-fetch-test--make-response-buffer (status-line body) + "Return a fresh buffer containing STATUS-LINE, a blank line, and BODY. +STATUS-LINE is an HTTP status line such as \"HTTP/1.1 200 OK\". BODY +is the response body as a string. The buffer is unibyte so that +multibyte handling is exercised end-to-end." + (let ((buf (generate-new-buffer " *gloss-fetch-test-response*"))) + (with-current-buffer buf + (set-buffer-multibyte nil) + (insert status-line "\n") + (insert "Content-Type: application/json\n") + (insert "\n") + (insert (encode-coding-string (or body "") 'utf-8))) + buf)) + +(defmacro gloss-fetch-test--with-mocked-url (response-fn &rest body) + "Run BODY with `url-retrieve-synchronously' replaced by RESPONSE-FN. +RESPONSE-FN takes the URL string and returns either a buffer (the +response) or nil (to simulate timeout / unreachable)." + (declare (indent 1) (debug t)) + `(cl-letf (((symbol-function 'url-retrieve-synchronously) + (lambda (url &rest _args) (funcall ,response-fn url)))) + ,@body)) + +(defun gloss-fetch-test--ok-response (body) + "Return a 200 OK response buffer with BODY." + (gloss-fetch-test--make-response-buffer "HTTP/1.1 200 OK" body)) + +(defun gloss-fetch-test--status-response (status-line &optional body) + "Return a response buffer with STATUS-LINE and optional BODY (default empty)." + (gloss-fetch-test--make-response-buffer status-line (or body ""))) + +(provide 'testutil-gloss-fetch) +;;; testutil-gloss-fetch.el ends here |
