1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
|
;;; test-pearl-query.el --- Tests for the general issue query -*- 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 `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 capped, newest-first comments by default."
(let ((pearl-fetch-comments-in-list t)
(pearl-list-comments-count-cap 25))
(should (string-match-p "comments(first: 26, orderBy: createdAt)[[:space:]]*{[[:space:]]*nodes"
(pearl--issues-query)))))
(ert-deftest test-pearl-issues-query-omits-comments-when-disabled ()
"With list comments disabled, the bulk query carries no comments selection."
(let ((pearl-fetch-comments-in-list nil))
(should-not (string-match-p "comments" (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
|