diff options
Diffstat (limited to 'tests/test-pearl-resolve.el')
| -rw-r--r-- | tests/test-pearl-resolve.el | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/tests/test-pearl-resolve.el b/tests/test-pearl-resolve.el new file mode 100644 index 0000000..d8dda7d --- /dev/null +++ b/tests/test-pearl-resolve.el @@ -0,0 +1,141 @@ +;;; test-pearl-resolve.el --- Tests for name->id resolution helpers -*- lexical-binding: t; -*- + +;; Copyright (C) 2026 Craig Jennings + +;; Author: Craig Jennings <c@cjennings.net> + +;; 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. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Tests for the per-team cached collection fetch (`--team-collection') and +;; the name->id resolver (`--resolve-team-id'), which back the field commands, +;; the ad-hoc filter, and saved queries. The HTTP boundary is stubbed; the +;; cache is reset around each test. + +;;; Code: + +(require 'test-bootstrap (expand-file-name "test-bootstrap.el")) +(require 'cl-lib) + +(defmacro test-pearl--with-clean-caches (&rest body) + "Run BODY with the Linear caches reset and an API key set." + (declare (indent 0)) + `(let ((pearl-api-key "test-key") + (pearl--cache-team-collections nil) + (pearl--cache-states nil) + (pearl--cache-teams nil) + (pearl--cache-issues nil)) + ,@body)) + +(defmacro test-pearl--counting-request (data counter &rest body) + "Run BODY with `request' stubbed to succeed with DATA, counting calls in COUNTER." + (declare (indent 2)) + `(cl-letf (((symbol-function 'request) + (lambda (_url &rest args) + (cl-incf ,counter) + (let ((cb (plist-get args :success))) + (when cb (funcall cb :data ,data)))))) + ,@body)) + +;;; --team-collection + +(ert-deftest test-pearl-team-collection-fetches-and-caches () + "A collection is fetched once and served from cache on the second call." + (test-pearl--with-clean-caches + (let ((calls 0)) + (test-pearl--counting-request + '((data (team (projects (nodes . [((id . "p1") (name . "Foo")) + ((id . "p2") (name . "Bar"))]))))) calls + (let ((first (pearl--team-collection 'projects "team-1"))) + (should (= 2 (length first))) + (should (string= "p1" (cdr (assoc 'id (car first))))) + (pearl--team-collection 'projects "team-1") + (should (= 1 calls))))))) + +(ert-deftest test-pearl-team-collection-force-refetches () + "A force refresh bypasses the cache and fetches again." + (test-pearl--with-clean-caches + (let ((calls 0)) + (test-pearl--counting-request + '((data (team (labels (nodes . [((id . "l1") (name . "bug"))]))))) calls + (pearl--team-collection 'labels "team-1") + (pearl--team-collection 'labels "team-1" t) + (should (= 2 calls)))))) + +(ert-deftest test-pearl-team-collection-keyed-by-team () + "Different teams cache separately." + (test-pearl--with-clean-caches + (let ((calls 0)) + (test-pearl--counting-request + '((data (team (members (nodes . [((id . "u1") (name . "Craig"))]))))) calls + (pearl--team-collection 'members "team-1") + (pearl--team-collection 'members "team-2") + (should (= 2 calls)))))) + +;;; --resolve-team-id + +(ert-deftest test-pearl-resolve-unique-name () + "A name with one match resolves to that id." + (test-pearl--with-clean-caches + (cl-letf (((symbol-function 'pearl--team-collection) + (lambda (&rest _) '(((id . "p1") (name . "Foo")) + ((id . "p2") (name . "Bar")))))) + (should (string= "p1" (pearl--resolve-team-id 'projects "Foo" "team-1"))) + ;; case-insensitive + (should (string= "p2" (pearl--resolve-team-id 'projects "bar" "team-1")))))) + +(ert-deftest test-pearl-resolve-no-match-is-nil () + "A name with no match resolves to nil." + (test-pearl--with-clean-caches + (cl-letf (((symbol-function 'pearl--team-collection) + (lambda (&rest _) '(((id . "p1") (name . "Foo")))))) + (should-not (pearl--resolve-team-id 'projects "Missing" "team-1"))))) + +(ert-deftest test-pearl-resolve-member-by-display-name-or-email () + "Members match on name, displayName, or email." + (test-pearl--with-clean-caches + (cl-letf (((symbol-function 'pearl--team-collection) + (lambda (&rest _) + '(((id . "u1") (name . "Craig Jennings") (displayName . "cj") + (email . "c@x.com")))))) + (should (string= "u1" (pearl--resolve-team-id 'members "cj" "team-1"))) + (should (string= "u1" (pearl--resolve-team-id 'members "c@x.com" "team-1")))))) + +(ert-deftest test-pearl-resolve-ambiguous-prompts () + "When several nodes share a name, the resolver prompts and returns the choice." + (test-pearl--with-clean-caches + (cl-letf (((symbol-function 'pearl--team-collection) + (lambda (&rest _) '(((id . "u1") (name . "Alex") (displayName . "alex-a")) + ((id . "u2") (name . "Alex") (displayName . "alex-b"))))) + ((symbol-function 'completing-read) + (lambda (_prompt collection &rest _) + ;; pick the entry whose id is u2 + (car (rassoc "u2" collection))))) + (should (string= "u2" (pearl--resolve-team-id 'members "Alex" "team-1")))))) + +;;; clear-cache + +(ert-deftest test-pearl-clear-cache-resets () + "Clearing the cache empties the collection and per-team caches." + (let ((pearl--cache-team-collections '(((projects . "t") . x))) + (pearl--cache-states '(("t" . y))) + (pearl--cache-teams '(z))) + (pearl-clear-cache) + (should-not pearl--cache-team-collections) + (should-not pearl--cache-states) + (should-not pearl--cache-teams))) + +(provide 'test-pearl-resolve) +;;; test-pearl-resolve.el ends here |
