;;; test-pearl-query.el --- Tests for the general issue query -*- lexical-binding: t; -*- ;; Copyright (C) 2026 Craig Jennings ;; Author: Craig Jennings ;; 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 . ;;; Commentary: ;; Tests for `pearl--query-issues-async' and the `--page-issues' pager, ;; with `--graphql-request-async' stubbed. Cover the query/variable ;; construction, pagination across pages, the page-cap truncation, and the ;; full set of result statuses (ok / empty / graphql-failed / request-failed). ;;; Code: (require 'test-bootstrap (expand-file-name "test-bootstrap.el")) (require 'cl-lib) (defun test-lq--page (nodes has-next &optional cursor) "Build a raw issues-page response with NODES, HAS-NEXT, and CURSOR." `((data (issues (nodes . ,(vconcat nodes)) (pageInfo (hasNextPage . ,(if has-next t :json-false)) (endCursor . ,(or cursor "c"))))))) ;;; construction (ert-deftest test-pearl-query-issues-construction () "The query targets issues(filter:) and passes the filter + default orderBy." (let (captured result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (query variables success-fn _error-fn) (setq captured (list query variables)) (funcall success-fn (test-lq--page '(((id . "i1"))) nil))))) (pearl--query-issues-async '(("assignee" ("isMe" ("eq" . t)))) (lambda (r) (setq result r))) (should (string-match-p "issues(filter:" (car captured))) (should (equal '(("assignee" ("isMe" ("eq" . t)))) (cdr (assoc "filter" (cadr captured))))) (should (string= "updatedAt" (cdr (assoc "orderBy" (cadr captured))))) (should (= 100 (cdr (assoc "first" (cadr captured)))))))) (ert-deftest test-pearl-query-issues-no-filter-omits-variable () "With a nil filter, the filter variable is omitted (no filter applied)." (let (vars) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q variables success-fn _e) (setq vars variables) (funcall success-fn (test-lq--page '() nil))))) (pearl--query-issues-async nil #'ignore) (should-not (assoc "filter" vars))))) (ert-deftest test-pearl-query-issues-order-by-override () "An explicit ORDER-BY overrides the updatedAt default." (let (vars) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q variables success-fn _e) (setq vars variables) (funcall success-fn (test-lq--page '() nil))))) (pearl--query-issues-async nil #'ignore 'createdAt) (should (string= "createdAt" (cdr (assoc "orderBy" vars))))))) ;;; result statuses (ert-deftest test-pearl-query-issues-single-page-ok () "A single page with issues yields an ok result carrying the raw nodes." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (funcall success-fn (test-lq--page '(((id . "i1")) ((id . "i2"))) nil))))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (eq 'ok (pearl--query-result-status result))) (should (= 2 (length (pearl--query-result-issues result)))) (should-not (pearl--query-result-truncated-p result))))) (ert-deftest test-pearl-query-issues-empty () "A page with no nodes yields an empty result, not a failure." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (funcall success-fn (test-lq--page '() nil))))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (eq 'empty (pearl--query-result-status result)))))) (ert-deftest test-pearl-query-issues-graphql-error () "A GraphQL error response surfaces as a graphql-failed result." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (funcall success-fn '((errors . (((message . "bad filter")))))))) ) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (eq 'graphql-failed (pearl--query-result-status result))) (should (string= "bad filter" (pearl--query-result-message result)))))) (ert-deftest test-pearl-query-issues-transport-error () "A transport failure (error callback) surfaces as request-failed." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v _success-fn error-fn) (funcall error-fn "boom" nil nil)))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (eq 'request-failed (pearl--query-result-status result)))))) ;;; pagination (ert-deftest test-pearl-query-issues-paginates () "Multiple pages accumulate; the cursor drives the next fetch." (let ((result nil) (calls 0)) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q variables success-fn _e) (setq calls (1+ calls)) (if (assoc "after" variables) (funcall success-fn (test-lq--page '(((id . "i2"))) nil)) (funcall success-fn (test-lq--page '(((id . "i1"))) t "cur")))))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (= 2 calls)) (should (eq 'ok (pearl--query-result-status result))) (should (= 2 (length (pearl--query-result-issues result))))))) (ert-deftest test-pearl-query-issues-cap-truncates () "Hitting the page cap stops paging and marks the result truncated." (let ((result nil) (calls 0) (pearl-max-issue-pages 3)) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (setq calls (1+ calls)) (funcall success-fn (test-lq--page '(((id . "x"))) t "cur"))))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (pearl--query-result-truncated-p result)) (should (= 3 calls)) (should (= 3 (length (pearl--query-result-issues result))))))) ;;; the bulk query fetches comments so the list can render them (ert-deftest test-pearl-issues-query-requests-comments () "The bulk issues query selects comments, so a populated list shows them." (should (string-match-p "comments[[:space:]]*{[[:space:]]*nodes" pearl--issues-query))) ;;; malformed remote page shapes (ert-deftest test-pearl-query-issues-missing-issues-key-is-empty () "A success payload lacking data.issues yields an empty result, not a Lisp error." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (funcall success-fn '((data . nil)))))) (pearl--query-issues-async nil (lambda (r) (setq result r))) (should (eq 'empty (pearl--query-result-status result))) (should-not (pearl--query-result-issues result))))) (ert-deftest test-pearl-query-view-null-customview-no-lisp-error () "A response with data.customView nil yields a structured result, not a Lisp error." (let (result) (cl-letf (((symbol-function 'pearl--graphql-request-async) (lambda (_q _v success-fn _e) (funcall success-fn '((data (customView . nil))))))) (pearl--query-view-async "v1" (lambda (r) (setq result r))) ;; a deleted / no-access view must not crash the render boundary (should (pearl--query-result-status result))))) (ert-deftest test-pearl-page-issues-missing-cursor-does-not-loop () "has-next-page t with a nil end-cursor terminates (bounded by max-pages), not forever." (let ((calls 0) result (pearl-max-issue-pages 3)) (pearl--page-issues (lambda (_after page-cb) (cl-incf calls) (funcall page-cb (list :issues '(((id . "x"))) :has-next-page t :end-cursor nil))) (lambda (r) (setq result r)) 3) (should (= 3 calls)) (should (pearl--query-result-truncated-p result)))) (ert-deftest test-pearl-node-list-non-list-nodes-is-empty () "A connection whose nodes is neither a vector nor a list yields the empty list." (should (null (pearl--node-list '((nodes . "oops"))))) (should (null (pearl--node-list '((nodes . 42))))) (should (null (pearl--node-list nil)))) (provide 'test-pearl-query) ;;; test-pearl-query.el ends here