22# =============================================================================
33# tests/test_common.bats
44#
5- # Unit tests for lib/github-common.sh — pure-logic functions and gh_api
6- # sentinel returns. No real network calls are made; curl is mocked per test.
5+ # Unit tests for lib/github-common.sh — pure-logic functions and API helpers.
6+ # No real network calls are made; curl and gh are mocked per test via a
7+ # temporary directory prepended to PATH.
78#
89# Requirements:
910# - bats (https://github.com/bats-core/bats-core / apt install bats)
1415
1516LIB_PATH=" ${BATS_TEST_DIRNAME} /../lib/github-common.sh"
1617
17- # Per-test mock binary directory; prepend to PATH in subshells that need mocking
1818MOCK_BIN=" "
1919
2020setup () {
2121 MOCK_BIN=" $( mktemp -d) "
22+ # Default gh mock: fails all calls so GITHUB_TOKEN is not auto-resolved
23+ printf ' #!/bin/sh\nexit 1\n' > " $MOCK_BIN /gh"
24+ chmod +x " $MOCK_BIN /gh"
2225}
2326
2427teardown () {
2528 rm -rf " $MOCK_BIN "
2629}
2730
28- # Write a mock curl that outputs <body>\n<status_code> and ignores all arguments.
29- # gh_api captures: body=$(curl -s -w "\n%{http_code}" ...) then splits on last line.
30- # Body and status are passed via env vars to avoid quoting issues with JSON content.
31+ # Install the universal curl mock. Passes response data via env vars so no
32+ # quoting issues arise with JSON or special characters in the body.
33+ # $1 HTTP status code (default: 200)
34+ # $2 Response body (default: empty)
35+ # $3 Link: next URL (default: empty = last page)
3136_mock_curl () {
32- local code=" $1 " body=" ${2:- } "
37+ local code=" ${1 :- 200} " body=" ${2:- } " link= " ${3 :- }"
3338 export MOCK_CURL_CODE=" $code "
3439 export MOCK_CURL_BODY=" $body "
35- printf ' #!/bin/sh\nprintf "%%s\\n%%s" "$MOCK_CURL_BODY" "$MOCK_CURL_CODE"\n ' \
36- > " $MOCK_BIN /curl"
40+ export MOCK_CURL_LINK= " $link "
41+ cp " ${BATS_TEST_DIRNAME} /mock_curl.sh " " $MOCK_BIN /curl"
3742 chmod +x " $MOCK_BIN /curl"
3843}
3944
45+ # ─── err ──────────────────────────────────────────────────────────────────────
46+
47+ @test " err: exits 1" {
48+ run bash -c " GITHUB_TOKEN=x source '${LIB_PATH} ' 2>/dev/null; err 'something went wrong'"
49+ [ " $status " -eq 1 ]
50+ }
51+
52+ @test " err: output contains the supplied message" {
53+ run bash -c " GITHUB_TOKEN=x source '${LIB_PATH} ' 2>/dev/null; err 'something went wrong'" 2>&1
54+ [[ " $output " == * " something went wrong" * ]]
55+ }
56+
4057# ─── validate_slug ────────────────────────────────────────────────────────────
4158
4259@test " validate_slug: alphanumeric slug passes" {
@@ -103,6 +120,110 @@ _mock_curl() {
103120 [ " $status " -eq 1 ]
104121}
105122
123+ # ─── configure_gh_auth ────────────────────────────────────────────────────────
124+
125+ @test " configure_gh_auth: passes and exports GH_TOKEN when GITHUB_TOKEN is set" {
126+ run bash -c "
127+ export PATH='${MOCK_BIN} :${PATH} '
128+ export GITHUB_TOKEN=mytoken
129+ source '${LIB_PATH} ' 2>/dev/null
130+ configure_gh_auth
131+ [ \"\$ GH_TOKEN\" = 'mytoken' ]
132+ "
133+ [ " $status " -eq 0 ]
134+ }
135+
136+ @test " configure_gh_auth: exits 1 when GITHUB_TOKEN unset and gh auth fails" {
137+ run bash -c "
138+ export PATH='${MOCK_BIN} :${PATH} '
139+ unset GITHUB_TOKEN
140+ source '${LIB_PATH} ' 2>/dev/null
141+ configure_gh_auth
142+ "
143+ [ " $status " -eq 1 ]
144+ }
145+
146+ # ─── validate_token / validate_github_token ───────────────────────────────────
147+
148+ @test " validate_token: exits 0 on HTTP 200" {
149+ _mock_curl 200
150+ run bash -c "
151+ export PATH='${MOCK_BIN} :${PATH} '
152+ export GITHUB_TOKEN=fake
153+ export API_URL_PREFIX=https://api.github.com
154+ source '${LIB_PATH} ' 2>/dev/null
155+ validate_token GITHUB_TOKEN
156+ "
157+ [ " $status " -eq 0 ]
158+ }
159+
160+ @test " validate_token: exits 1 on HTTP 401" {
161+ _mock_curl 401
162+ run bash -c "
163+ export PATH='${MOCK_BIN} :${PATH} '
164+ export GITHUB_TOKEN=fake
165+ export API_URL_PREFIX=https://api.github.com
166+ source '${LIB_PATH} ' 2>/dev/null
167+ validate_token GITHUB_TOKEN
168+ "
169+ [ " $status " -eq 1 ]
170+ }
171+
172+ @test " validate_github_token: emits warning for non-GitHub API_URL_PREFIX" {
173+ _mock_curl 200
174+ run bash -c "
175+ export PATH='${MOCK_BIN} :${PATH} '
176+ export GITHUB_TOKEN=fake
177+ export API_URL_PREFIX=https://not-github.example.com
178+ source '${LIB_PATH} ' 2>/dev/null
179+ validate_github_token
180+ "
181+ [ " $status " -eq 0 ]
182+ [[ " $output " == * " does not look like" * ]]
183+ }
184+
185+ @test " validate_github_token: no warning for api.github.com" {
186+ _mock_curl 200
187+ run bash -c "
188+ export PATH='${MOCK_BIN} :${PATH} '
189+ export GITHUB_TOKEN=fake
190+ export API_URL_PREFIX=https://api.github.com
191+ source '${LIB_PATH} ' 2>/dev/null
192+ validate_github_token
193+ "
194+ [ " $status " -eq 0 ]
195+ [[ " $output " != * " does not look like" * ]]
196+ }
197+
198+ # ─── get_repo_page_count ──────────────────────────────────────────────────────
199+
200+ @test " get_repo_page_count: returns 1 when there is no Link header" {
201+ _mock_curl 200
202+ run bash -c "
203+ export PATH='${MOCK_BIN} :${PATH} '
204+ export GITHUB_TOKEN=fake
205+ export API_URL_PREFIX=https://api.github.com
206+ source '${LIB_PATH} ' 2>/dev/null
207+ get_repo_page_count 'https://api.github.com/orgs/test/repos?per_page=100'
208+ "
209+ [ " $status " -eq 0 ]
210+ [ " $output " = " 1" ]
211+ }
212+
213+ @test " get_repo_page_count: returns last page number from Link header" {
214+ # Body contains a Link header line — get_repo_page_count greps stdout for &page=N
215+ _mock_curl 200 ' Link: <https://api.github.com/orgs/test/repos?per_page=100&page=7>; rel="last"'
216+ run bash -c "
217+ export PATH='${MOCK_BIN} :${PATH} '
218+ export GITHUB_TOKEN=fake
219+ export API_URL_PREFIX=https://api.github.com
220+ source '${LIB_PATH} ' 2>/dev/null
221+ get_repo_page_count 'https://api.github.com/orgs/test/repos?per_page=100'
222+ "
223+ [ " $status " -eq 0 ]
224+ [ " $output " = " 7" ]
225+ }
226+
106227# ─── gh_api sentinels ─────────────────────────────────────────────────────────
107228
108229@test " gh_api: returns __404__ on HTTP 404" {
@@ -126,3 +247,58 @@ _mock_curl() {
126247 [ " $status " -eq 0 ]
127248 [ " $output " = ' {"login":"test-org"}' ]
128249}
250+
251+ @test " gh_api: prepends API_URL_PREFIX when path starts with /" {
252+ _mock_curl 200 ' {"ok":true}'
253+ run bash -c "
254+ export PATH='${MOCK_BIN} :${PATH} '
255+ export GITHUB_TOKEN=fake
256+ export API_URL_PREFIX=https://api.github.com
257+ source '${LIB_PATH} ' 2>/dev/null
258+ result=\$ (gh_api '/some/path')
259+ [ \"\$ result\" = '{\" ok\" :true}' ]
260+ "
261+ [ " $status " -eq 0 ]
262+ }
263+
264+ # ─── gh_api_paginate ──────────────────────────────────────────────────────────
265+
266+ @test " gh_api_paginate: exits 0 silently on HTTP 404" {
267+ _mock_curl 404
268+ run bash -c "
269+ export PATH='${MOCK_BIN} :${PATH} '
270+ export GITHUB_TOKEN=fake
271+ export API_URL_PREFIX=https://api.github.com
272+ source '${LIB_PATH} ' 2>/dev/null
273+ gh_api_paginate '/orgs/nonexistent/repos'
274+ "
275+ [ " $status " -eq 0 ]
276+ [ -z " $output " ]
277+ }
278+
279+ @test " gh_api_paginate: exits 0 silently on HTTP 422" {
280+ _mock_curl 422
281+ run bash -c "
282+ export PATH='${MOCK_BIN} :${PATH} '
283+ export GITHUB_TOKEN=fake
284+ export API_URL_PREFIX=https://api.github.com
285+ source '${LIB_PATH} ' 2>/dev/null
286+ gh_api_paginate '/orgs/test/repos'
287+ "
288+ [ " $status " -eq 0 ]
289+ [ -z " $output " ]
290+ }
291+
292+ @test " gh_api_paginate: outputs items from a single page" {
293+ _mock_curl 200 ' [{"name":"repo-a"},{"name":"repo-b"}]'
294+ run bash -c "
295+ export PATH='${MOCK_BIN} :${PATH} '
296+ export GITHUB_TOKEN=fake
297+ export API_URL_PREFIX=https://api.github.com
298+ source '${LIB_PATH} ' 2>/dev/null
299+ gh_api_paginate '/orgs/test/repos'
300+ "
301+ [ " $status " -eq 0 ]
302+ [[ " $output " == * ' "name":"repo-a"' * ]]
303+ [[ " $output " == * ' "name":"repo-b"' * ]]
304+ }
0 commit comments