mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 16:31:05 +02:00
135 lines
6.4 KiB
Lua
135 lines
6.4 KiB
Lua
-- HostShell's HTTP error reporting. No pokered cite: host transport is
|
|
-- port-only plumbing.
|
|
--
|
|
-- A user hit the launcher's mod index against a rate-limited GitHub and all
|
|
-- they got was one line on the terminal:
|
|
--
|
|
-- curl: (56) The requested URL returned error: 403
|
|
--
|
|
-- That is curl talking to its own stderr. It names no URL, so with an index
|
|
-- feed, a releases API and a page of thumbnails all in flight there was no way
|
|
-- to tell WHICH fetch failed, and the caller upstream got a generic "empty
|
|
-- response" that said even less. HostShell now merges curl's stderr into the
|
|
-- pipe and asks for the status with --write-out, so every failure names its
|
|
-- URL and its HTTP code, and a 403 body ("API rate limit exceeded") reaches
|
|
-- the launcher's notice line where a user can act on it.
|
|
--
|
|
-- The seam is io.popen: these cases stub it to replay exactly what curl writes
|
|
-- for each outcome, which is the only way to pin the parsing without a network
|
|
-- and a cooperating server.
|
|
-- luajit tests/engine/host_shell_fetch_errors.lua
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check = T.check
|
|
love = love or require("tests.love_stub")
|
|
|
|
local HostShell = require("src.core.HostShell")
|
|
|
|
-- The marker HostShell asks curl to print before the status code. Spelled
|
|
-- here the way it arrives (a real newline), not the way it is passed to curl
|
|
-- (a backslash-n escape curl expands itself).
|
|
local MARK = "\n__gen1recomp_http__"
|
|
|
|
-- Replay `output` as the next popen's whole stdout. `curl --version` is
|
|
-- answered separately so HostShell.haveCurl agrees a transport exists.
|
|
local realPopen = io.popen
|
|
local lastCommand
|
|
local function stubPopen(output)
|
|
io.popen = function(cmd, mode)
|
|
lastCommand = cmd
|
|
if cmd:find("--version", 1, true) then
|
|
return { read = function() return "curl 8.7.1 (test)" end,
|
|
close = function() return true end }
|
|
end
|
|
return { read = function() return output end,
|
|
close = function() return true end }
|
|
end
|
|
end
|
|
local function restorePopen() io.popen = realPopen end
|
|
|
|
local URL = "https://api.github.com/repos/example/thing/releases"
|
|
|
|
-- ------------------------------------------------------------------- 200
|
|
stubPopen('{"tag_name":"v1.2.3"}' .. MARK .. "200")
|
|
local body, err = HostShell.httpGet(URL, "gen1recomp", nil, 10)
|
|
check(body == '{"tag_name":"v1.2.3"}',
|
|
"a 200 returns the body with the status marker stripped: " .. tostring(body))
|
|
check(err == nil, "a 200 reports no error")
|
|
check(lastCommand:find("%-w ") ~= nil,
|
|
"the GET asks curl for the status code")
|
|
check(lastCommand:find("2>&1", 1, true) ~= nil,
|
|
"the GET captures curl's stderr instead of leaking it to the terminal")
|
|
check(lastCommand:find(" -f", 1, true) == nil,
|
|
"the GET does NOT pass -f: the error body is the diagnosis")
|
|
|
|
-- ------------------------------------------------------------------- 403
|
|
-- What GitHub actually sends when the launcher has burned its unauthenticated
|
|
-- hourly allowance, with curl's own stderr merged in ahead of it.
|
|
stubPopen('{"message":"API rate limit exceeded for 203.0.113.7."}'
|
|
.. MARK .. "403")
|
|
local body403, err403 = HostShell.httpGet(URL, "gen1recomp", nil, 10)
|
|
check(body403 == nil, "a 403 is a failure, not a body")
|
|
check(err403:find(URL, 1, true) ~= nil,
|
|
"a 403 names the URL that failed: " .. tostring(err403))
|
|
check(err403:find("403", 1, true) ~= nil, "a 403 names the status code")
|
|
check(err403:find("rate limit", 1, true) ~= nil,
|
|
"a 403 carries the server's own explanation through to the caller")
|
|
|
|
-- --------------------------------------------------- no response at all
|
|
-- DNS failure: curl writes its complaint and a http_code of 0. Zero is not a
|
|
-- status, and reporting "HTTP 0" would bury the only useful line there is.
|
|
stubPopen("curl: (6) Could not resolve host: nope.invalid" .. MARK .. "0")
|
|
local bodyDns, errDns = HostShell.httpGet("https://nope.invalid/x", "ua", nil, 10)
|
|
check(bodyDns == nil, "an unresolvable host is a failure")
|
|
check(errDns:find("HTTP 0", 1, true) == nil,
|
|
"a no-response failure is not reported as HTTP 0: " .. tostring(errDns))
|
|
check(errDns:find("https://nope.invalid/x", 1, true) ~= nil,
|
|
"an unresolvable host still names the URL")
|
|
check(errDns:find("Could not resolve", 1, true) ~= nil,
|
|
"an unresolvable host reports curl's own reason")
|
|
|
|
-- --------------------------------------------- a body containing the marker
|
|
-- The status is cut from the LAST marker only, so a payload that happens to
|
|
-- contain the token keeps every byte of its content.
|
|
local sneaky = "prefix" .. MARK .. "999" .. "suffix"
|
|
stubPopen(sneaky .. MARK .. "200")
|
|
local bodySneaky = HostShell.httpGet(URL, "gen1recomp", nil, 10)
|
|
check(bodySneaky == sneaky,
|
|
"only the trailing status marker is stripped: " .. tostring(bodySneaky))
|
|
|
|
-- ------------------------------------------------------------- downloads
|
|
-- The download branch keeps -f (no error body is written to the file), but it
|
|
-- must still name the URL and the code rather than "download failed".
|
|
stubPopen("curl: (56) The requested URL returned error: 403" .. MARK .. "403")
|
|
local ok, dlErr = HostShell.httpDownload(URL, "/tmp/gen1recomp-test.bin",
|
|
"gen1recomp", nil, 10)
|
|
check(ok == nil, "a 403 download fails")
|
|
check(dlErr:find(URL, 1, true) ~= nil,
|
|
"a failed download names the URL: " .. tostring(dlErr))
|
|
check(dlErr:find("403", 1, true) ~= nil, "a failed download names the code")
|
|
|
|
stubPopen(MARK .. "200")
|
|
local ok2, dlErr2 = HostShell.httpDownload(URL, "/tmp/gen1recomp-test.bin",
|
|
"gen1recomp", nil, 10)
|
|
check(ok2 == true, "a 200 download succeeds: " .. tostring(dlErr2))
|
|
|
|
restorePopen()
|
|
|
|
-- ---------------------------------------------------------------- pclose
|
|
-- Every pipe HostShell hands out must be closed through pclose: a bare
|
|
-- pipe:close() from one thread can free a FILE while another thread's popen
|
|
-- is walking libc's stream list, and that thread never wakes up again (the
|
|
-- launcher freezing on close after a visit to the mod tabs). Nothing here can
|
|
-- exercise the race headlessly -- the test stub has no love.thread -- so this
|
|
-- pins the entry point's existence and its tolerance of junk.
|
|
check(type(HostShell.pclose) == "function", "HostShell exposes pclose")
|
|
local closed = false
|
|
HostShell.pclose({ close = function() closed = true return true end })
|
|
check(closed, "pclose closes the pipe it is given")
|
|
local okNil = pcall(HostShell.pclose, nil)
|
|
check(okNil, "pclose on nil is a no-op rather than an error")
|
|
|
|
T.finish("host shell fetch errors")
|