Skip to content

Commit 26bc287

Browse files
committed
Port PoB 2 trader Oauth API
1 parent 7e12149 commit 26bc287

10 files changed

Lines changed: 6623 additions & 362 deletions

File tree

runtime/lua/sha2.lua

Lines changed: 5675 additions & 0 deletions
Large diffs are not rendered by default.

runtime/lua/socket.lua

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
-----------------------------------------------------------------------------
2+
-- LuaSocket helper module
3+
-- Author: Diego Nehab
4+
-----------------------------------------------------------------------------
5+
6+
-----------------------------------------------------------------------------
7+
-- Declare module and import dependencies
8+
-----------------------------------------------------------------------------
9+
local base = _G
10+
local string = require("string")
11+
local math = require("math")
12+
local socket = require("socket.core")
13+
14+
local _M = socket
15+
16+
-----------------------------------------------------------------------------
17+
-- Exported auxiliar functions
18+
-----------------------------------------------------------------------------
19+
function _M.connect4(address, port, laddress, lport)
20+
return socket.connect(address, port, laddress, lport, "inet")
21+
end
22+
23+
function _M.connect6(address, port, laddress, lport)
24+
return socket.connect(address, port, laddress, lport, "inet6")
25+
end
26+
27+
function _M.bind(host, port, backlog)
28+
if host == "*" then host = "0.0.0.0" end
29+
local addrinfo, err = socket.dns.getaddrinfo(host);
30+
if not addrinfo then return nil, err end
31+
local sock, res
32+
err = "no info on address"
33+
for i, alt in base.ipairs(addrinfo) do
34+
if alt.family == "inet" then
35+
sock, err = socket.tcp4()
36+
else
37+
sock, err = socket.tcp6()
38+
end
39+
if not sock then return nil, err end
40+
sock:setoption("reuseaddr", true)
41+
res, err = sock:bind(alt.addr, port)
42+
if not res then
43+
sock:close()
44+
else
45+
res, err = sock:listen(backlog)
46+
if not res then
47+
sock:close()
48+
else
49+
return sock
50+
end
51+
end
52+
end
53+
return nil, err
54+
end
55+
56+
_M.try = _M.newtry()
57+
58+
function _M.choose(table)
59+
return function(name, opt1, opt2)
60+
if base.type(name) ~= "string" then
61+
name, opt1, opt2 = "default", name, opt1
62+
end
63+
local f = table[name or "nil"]
64+
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
65+
else return f(opt1, opt2) end
66+
end
67+
end
68+
69+
-----------------------------------------------------------------------------
70+
-- Socket sources and sinks, conforming to LTN12
71+
-----------------------------------------------------------------------------
72+
-- create namespaces inside LuaSocket namespace
73+
local sourcet, sinkt = {}, {}
74+
_M.sourcet = sourcet
75+
_M.sinkt = sinkt
76+
77+
_M.BLOCKSIZE = 2048
78+
79+
sinkt["close-when-done"] = function(sock)
80+
return base.setmetatable({
81+
getfd = function() return sock:getfd() end,
82+
dirty = function() return sock:dirty() end
83+
}, {
84+
__call = function(self, chunk, err)
85+
if not chunk then
86+
sock:close()
87+
return 1
88+
else return sock:send(chunk) end
89+
end
90+
})
91+
end
92+
93+
sinkt["keep-open"] = function(sock)
94+
return base.setmetatable({
95+
getfd = function() return sock:getfd() end,
96+
dirty = function() return sock:dirty() end
97+
}, {
98+
__call = function(self, chunk, err)
99+
if chunk then return sock:send(chunk)
100+
else return 1 end
101+
end
102+
})
103+
end
104+
105+
sinkt["default"] = sinkt["keep-open"]
106+
107+
_M.sink = _M.choose(sinkt)
108+
109+
sourcet["by-length"] = function(sock, length)
110+
return base.setmetatable({
111+
getfd = function() return sock:getfd() end,
112+
dirty = function() return sock:dirty() end
113+
}, {
114+
__call = function()
115+
if length <= 0 then return nil end
116+
local size = math.min(socket.BLOCKSIZE, length)
117+
local chunk, err = sock:receive(size)
118+
if err then return nil, err end
119+
length = length - string.len(chunk)
120+
return chunk
121+
end
122+
})
123+
end
124+
125+
sourcet["until-closed"] = function(sock)
126+
local done
127+
return base.setmetatable({
128+
getfd = function() return sock:getfd() end,
129+
dirty = function() return sock:dirty() end
130+
}, {
131+
__call = function()
132+
if done then return nil end
133+
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
134+
if not err then return chunk
135+
elseif err == "closed" then
136+
sock:close()
137+
done = 1
138+
return partial
139+
else return nil, err end
140+
end
141+
})
142+
end
143+
144+
145+
sourcet["default"] = sourcet["until-closed"]
146+
147+
_M.source = _M.choose(sourcet)
148+
149+
return _M

spec/System/TestTradeQueryCurrency_spec.lua

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,60 +5,56 @@ describe("TradeQuery Currency Conversion", function()
55
mock_tradeQuery = new("TradeQuery", { itemsTab = {} })
66
end)
77

8-
-- test case for commit: "Skip callback on errors to prevent incomplete conversions"
9-
describe("FetchCurrencyConversionTable", function()
10-
-- Pass: Callback not called on error
11-
-- Fail: Callback called, indicating partial data risk
12-
it("skips callback on error", function()
13-
local orig_launch = launch
14-
local spy = { called = false }
15-
launch = {
16-
DownloadPage = function(url, callback, opts)
17-
callback(nil, "test error")
18-
end
19-
}
20-
mock_tradeQuery:FetchCurrencyConversionTable(function()
21-
spy.called = true
22-
end)
23-
launch = orig_launch
24-
assert.is_false(spy.called)
25-
end)
26-
end)
27-
28-
describe("ConvertCurrencyToChaos", function()
29-
-- Pass: Ceils amount to integer (e.g., 4.9 -> 5)
30-
-- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals
8+
describe("ConvertCurrencyToDivs", function()
9+
-- Pass: Calculates price in divs
10+
-- Fail: Wrong value or nil, indicating broken rounding/baseline logic
3111
it("handles chaos currency", function()
32-
mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } }
12+
mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 0.1 } }
3313
mock_tradeQuery.pbLeague = "league"
34-
local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9)
35-
assert.are.equal(result, 5)
14+
local result = mock_tradeQuery:ConvertCurrencyToDivs("chaos", 5)
15+
assert.are.equal(result, 0.5)
3616
end)
3717

3818
-- Pass: Returns nil without crash
3919
-- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions
4020
it("returns nil for unmapped", function()
41-
local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10)
21+
local result = mock_tradeQuery:ConvertCurrencyToDivs("exotic", 10)
4222
assert.is_nil(result)
4323
end)
4424
end)
4525

4626
describe("PriceBuilderProcessPoENinjaResponse", function()
4727
-- Pass: Processes without error, restoring map while adding a notice
4828
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions
49-
it("handles unmapped currency", function()
29+
it("handles empty response", function()
5030
local orig_conv = mock_tradeQuery.currencyConversionTradeMap
5131
mock_tradeQuery.currencyConversionTradeMap = { div = "id" }
5232
mock_tradeQuery.pbLeague = "league"
5333
mock_tradeQuery.pbCurrencyConversion = { league = {} }
54-
mock_tradeQuery.controls.pbNotice = { label = ""}
55-
local resp = { exotic = 10 }
56-
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp)
34+
mock_tradeQuery.controls.pbNotice = { label = "" }
35+
local resp = { lines = { }}
36+
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines)
5737
-- No crash expected
5838
assert.is_true(true)
5939
assert.is_true(mock_tradeQuery.controls.pbNotice.label == "No currencies received from PoE Ninja")
6040
mock_tradeQuery.currencyConversionTradeMap = orig_conv
6141
end)
42+
43+
-- Pass: Processes without error, restoring map while adding a notice
44+
-- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions
45+
it("handles empty response", function()
46+
local orig_conv = mock_tradeQuery.currencyConversionTradeMap
47+
mock_tradeQuery.currencyConversionTradeMap = { div = "id" }
48+
mock_tradeQuery.pbLeague = "league"
49+
mock_tradeQuery.pbCurrencyConversion = { league = {} }
50+
mock_tradeQuery.controls.pbNotice = { label = "" }
51+
local resp = { lines = { { malformedLine = "lol"} }}
52+
mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp.lines)
53+
-- No crash expected
54+
assert.is_true(true)
55+
assert.is_true(mock_tradeQuery.controls.pbNotice.label == "Currencies not updated: malformed PoE Ninja response")
56+
mock_tradeQuery.currencyConversionTradeMap = orig_conv
57+
end)
6258
end)
6359

6460
describe("GetTotalPriceString", function()

spec/System/TestTradeQueryRequests_spec.lua

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,42 @@ describe("TradeQueryRequests", function()
6565
launch = orig_launch
6666
end)
6767

68+
-- Pass: Does not crash on 401, and passes error message
69+
-- Fail: Crash, or returned error is wrong
70+
it("does not crash on 401", function()
71+
local json = '"{"error":"invalid_token","error_description":"The access token provided is invalid or has expired"}"'
72+
local header = [[HTTP/1.1 401 Unauthorized
73+
Date: Fri, 24 Apr 2026 07:30:38 GMT
74+
Content-Type: application/json
75+
Transfer-Encoding: chunked
76+
Connection: keep-alive
77+
Server: cloudflare
78+
WWW-Authenticate: Bearer realm="pathofexile:production", error="invalid_token", error_description="The access token provided is invalid or has expired"
79+
Cache-Control: no-store
80+
Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]]
81+
local orig_launch = launch
82+
launch = {
83+
DownloadPage = function(url, onComplete, opts)
84+
onComplete({ body = json, header = header }, nil)
85+
end
86+
}
87+
table.insert(requests.requestQueue.search, {
88+
url = "test",
89+
callback = function(body, msg)
90+
assert.are.equal(body, json)
91+
assert.are.equal(msg, "Response code: 401\nAuthorization is invalid. Please Re-Log and reset")
92+
end,
93+
retryTime = nil
94+
})
95+
local function mock_next_time(self, policy, time)
96+
return time - 1
97+
end
98+
mock_limiter.NextRequestTime = mock_next_time
99+
requests:ProcessQueue()
100+
assert.are.equal(#requests.requestQueue.search, 0)
101+
launch = orig_launch
102+
end)
103+
68104
-- Pass: Retries with increasing backoff up to cap, preventing infinite loops
69105
-- Fail: No backoff or uncapped, indicating retry bug, risking API bans
70106
it("retries on 429 with exponential backoff", function()
@@ -192,4 +228,4 @@ describe("TradeQueryRequests", function()
192228
requests.FetchResultBlock = orig_fetchBlock
193229
end)
194230
end)
195-
end)
231+
end)

0 commit comments

Comments
 (0)