Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion spec/System/TestCommon_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,4 @@ describe("Common", function()
-- common.classes.StupidClass = nil
-- end)
end)
end)
end)
27 changes: 16 additions & 11 deletions spec/System/TestCompareBuySimilar_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,13 @@ Implicits: 1
controls.mod1Check.state = true
controls.mod1Check.changeFunc(true)
controls.search.onClick()
local queryJson = copiedUrl:match("%?q=(.*)"):gsub("%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
local query = require("dkjson").decode(queryJson)
-- disabled for now due to headless wrapper lacking zlib bindings
-- local queryB64 = copiedUrl:match("Test%%20League/(.*)$"):gsub("%%(%x%x)", function(hex)
-- return string.char(tonumber(hex, 16))
-- end)
-- local query = require("dkjson").decode(require("Classes.TradeHelpers").B64GzipDecode(queryB64))

assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.query.stats)
-- assert.same({ { type = "and", filters = { { id = "explicit.stat_1526933524" } } } }, query.stats)
end)

it("rebuilds the URL when league and listed status change", function()
Expand All @@ -165,13 +166,17 @@ Implicits: 1
controls.leagueDrop:SetSel(2)
controls.search.onClick()
assert.not_equal(initialUrl, copiedUrl)
assert.is_truthy(copiedUrl:find("/Standard?", 1, true))
local standardUrl = copiedUrl
assert.is_truthy(copiedUrl:find("/Standard/", 1, true))

controls.listedDrop:SetSel(4)
controls.search.onClick()
assert.not_equal(standardUrl, copiedUrl)
assert.is_truthy(copiedUrl:find("any", 1, true))
-- disabled for now due to headless wrapper lacking zlib bindings
-- local standardUrl = copiedUrl

-- controls.listedDrop:SetSel(4)
-- controls.search.onClick()
-- assert.not_equal(standardUrl, copiedUrl)
-- local b64 = copiedUrl:match("Standard/(.-)$")
-- local json = require("Classes.TradeHelpers").B64GzipDecode(b64)
-- assert.is_truthy(json:find("any", 1, true))
end)

it("persists popup selector choices", function()
Expand Down
4 changes: 2 additions & 2 deletions spec/System/TestTradeQueryGenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -148,8 +148,8 @@ describe("TradeQueryGenerator", function()
}
queryGen.tradeTypeIndex = 1
local query
queryGen.requesterCallback = function(_, queryJson)
query = require("dkjson").decode(queryJson).query
queryGen.requesterCallback = function(_, queryTable)
query = queryTable
end
queryGen:FinishQuery()

Expand Down
154 changes: 153 additions & 1 deletion spec/System/TestTradeQueryRequests_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,158 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]]
end)
end)

describe("SearchWithURL", function()
local dkjson = require "dkjson"
local tradeHelpers = require "Classes.TradeHelpers"
local originalDecode, urlRequests, decodedQuery
local url = "https://www.pathofexile.com/trade2/search/poe2/Standard/encoded"

before_each(function()
urlRequests = new("TradeQueryRequests"):TradeQueryRequests(mock_limiter)
originalDecode = tradeHelpers.B64GzipDecode
-- Compression is provided by SimpleGraphic, which the headless wrapper stubs.
tradeHelpers.B64GzipDecode = function() return decodedQuery end
end)

after_each(function()
tradeHelpers.B64GzipDecode = originalDecode
end)

it("fetches ordinary searches by price and returns their query", function()
decodedQuery = [[{"stats":[{"type":"and","filters":[{"id":"explicit.stat_3299347043","value":{"min":1}}]}]}]]
local expectedQuery = dkjson.decode(decodedQuery)
local fetchedItems = { { id = "item1" } }
urlRequests.FetchResults = function(_, ids, queryId, callback)
assert.same({ "item1" }, ids)
assert.are.equal("searchId", queryId)
callback(fetchedItems)
end
local calls = 0
urlRequests:SearchWithURL(url, function(items, err, query)
calls = calls + 1
assert.is_nil(err)
assert.same(fetchedItems, items)
assert.same({ query = expectedQuery, sort = { price = "asc" } }, dkjson.decode(query))
end)
local search = table.remove(urlRequests.requestQueue.search, 1)
assert.are.equal("https://www.pathofexile.com/api/trade2/search/poe2/Standard", search.url)
search.callback([[{"total":1,"result":["item1"],"id":"searchId"}]])
assert.are.equal(1, calls)
assert.are.equal(0, #urlRequests.requestQueue.search)
end)

it("relays ordinary search errors to the caller", function()
decodedQuery = [[{"stats":[{"type":"and","filters":[]}]}]]
local calls = 0
urlRequests:SearchWithURL(url, function(items, err, query)
calls = calls + 1
assert.is_nil(items)
assert.are.equal("Response code: 403", err)
assert.is_table(dkjson.decode(query).query)
end)
table.remove(urlRequests.requestQueue.search, 1).callback(nil, "Response code: 403")
assert.are.equal(1, calls)
end)

it("moves a later weighted group first while retaining the other filters", function()
decodedQuery = [[{"stats":[{"type":"count","value":{"min":1},"filters":[]},{"type":"weight","value":{"min":10},"filters":[]}]}]]
urlRequests:SearchWithURL(url, function() end)
local query = dkjson.decode(urlRequests.requestQueue.search[1].body)
assert.same({ ["statgroup.0"] = "desc" }, query.sort)
assert.are.equal("weight", query.query.stats[1].type)
assert.are.equal(10, query.query.stats[1].value.min)
assert.are.equal("count", query.query.stats[2].type)
assert.are.equal(1, query.query.stats[2].value.min)
end)

for _, case in ipairs({
{ name = "an explicit minimum with only five matches", bounds = { min = 2000 }, weight = 2100, total = 5 },
{ name = "negative weights with a maximum-only bound", bounds = { max = -50 }, weight = -60, total = 10000 },
{ name = "negative weights without bounds", weight = -60, total = 10000 },
{ name = "both bounds with exactly the fetch limit", bounds = { min = 100, max = 200 }, weight = 150, total = 10 },
}) do
it("fetches pasted searches once, preserving " .. case.name, function()
local filters = { { id = "explicit.stat_3299347043", value = { weight = case.weight < 0 and -1 or 1 } } }
local suppliedQuery = { stats = { { type = "weight", filters = filters, value = case.bounds } } }
decodedQuery = dkjson.encode(suppliedQuery)
local fetchedItems = { { id = "item1", weight = tostring(case.weight) } }
local fetchCalls, callbackCalls = 0, 0
urlRequests.FetchResults = function(_, ids, queryId, callback)
fetchCalls = fetchCalls + 1
assert.same({ "item1" }, ids)
assert.are.equal("searchId", queryId)
callback(fetchedItems)
end
urlRequests:SearchWithURL(url, function(items, err, query)
callbackCalls = callbackCalls + 1
assert.is_nil(err)
assert.same(fetchedItems, items)
assert.same({ query = suppliedQuery, sort = { ["statgroup.0"] = "desc" } }, dkjson.decode(query))
end)
assert.are.equal(1, #urlRequests.requestQueue.search)
local search = table.remove(urlRequests.requestQueue.search, 1)
assert.same(suppliedQuery, dkjson.decode(search.body).query)
search.callback(dkjson.encode({ total = case.total, result = { "item1" }, id = "searchId" }))
assert.are.equal(1, fetchCalls)
assert.are.equal(1, callbackCalls)
assert.are.equal(0, #urlRequests.requestQueue.search)
end)
end

it("reports no matches without relaxing a pasted minimum", function()
decodedQuery = [[{"stats":[{"type":"weight","value":{"min":2000},"filters":[]}]}]]
local calls = 0
urlRequests:SearchWithURL(url, function(items, err, query)
calls = calls + 1
assert.is_nil(items)
assert.are.equal("No Matching Results Found", err)
assert.are.equal(2000, dkjson.decode(query).query.stats[1].value.min)
end)
table.remove(urlRequests.requestQueue.search, 1).callback([[{"total":0,"result":[],"id":"emptySearch"}]])
assert.are.equal(1, calls)
assert.are.equal(0, #urlRequests.requestQueue.search)
end)

for _, malformed in ipairs({
[[{"stats":[]}]],
[[{"stats":[{"type":"and"},false]}]],
[[{"stats":[{}]}]],
[[42]],
[[not JSON]],
}) do
it("rejects malformed query " .. malformed .. " through the callback", function()
decodedQuery = malformed
local calls = 0
urlRequests:SearchWithURL(url, function(items, err)
calls = calls + 1
assert.is_nil(items)
assert.are.equal("URL is malformed", err)
end)
assert.are.equal(1, calls)
assert.are.equal(0, #urlRequests.requestQueue.search)
end)
end

it("rejects failed decompression through the callback", function()
decodedQuery = nil
local calls = 0
urlRequests:SearchWithURL(url, function(_, err)
calls = calls + 1
assert.are.equal("URL is malformed", err)
end)
assert.are.equal(1, calls)
end)

it("rejects an unrelated URL through the callback", function()
local calls = 0
urlRequests:SearchWithURL("https://example.com/", function(_, err)
calls = calls + 1
assert.are.equal("Invalid URL", err)
end)
assert.are.equal(1, calls)
end)
end)

describe("FetchResults", function()
-- Pass: Fetches exactly 10 from 11, in 1 block
-- Fail: Fetches wrong count/blocks, indicating batch limit violation, triggering rate limits
Expand Down Expand Up @@ -228,4 +380,4 @@ Strict-Transport-Security: max-age=63115200; includeSubDomains; preload]]
requests.FetchResultBlock = orig_fetchBlock
end)
end)
end)
end)
43 changes: 43 additions & 0 deletions spec/System/TestTradeQuery_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,49 @@ describe("TradeQuery", function ()
end)
end)

describe("purchase search filters", function()
local dkjson = require "dkjson"
local tradeHelpers = require "Classes.TradeHelpers"
local originalEncode, originalCopy, originalOpenURL, encodedQuery

before_each(function()
originalEncode, originalCopy, originalOpenURL = tradeHelpers.B64GzipEncode, Copy, OpenURL
tradeHelpers.B64GzipEncode = function(query)
encodedQuery = dkjson.decode(query)
return "encoded"
end
Copy, OpenURL = function() end, function() end
mock_tradeQuery.itemsTab.activeItemSet = {}
mock_tradeQuery.itemsTab.slots = {}
mock_tradeQuery.slotTables[1] = { slotName = "Ring 1" }
mock_tradeQuery:PriceItemRowDisplay(1, nil, 0, 20)
mock_tradeQuery.pbLeague = "Standard"
mock_tradeQuery.itemIndexTbl[1] = 1
mock_tradeQuery.resultTbl[1] = { { trader = "ExampleAccount", weight = "100", priceType = "~b/o" } }
end)

after_each(function()
tradeHelpers.B64GzipEncode, Copy, OpenURL = originalEncode, originalCopy, originalOpenURL
end)

for _, group in ipairs({
{ type = "and", filters = {} },
{ type = "count", value = { min = 1 }, filters = {} },
{ type = "weight", value = { min = 10 }, filters = {} },
}) do
it("preserves " .. group.type .. " group semantics when opening a purchase search", function()
mock_tradeQuery.lastQueries[1] = dkjson.encode({ query = { stats = { group } } })
mock_tradeQuery.controls.whisperButton1.onClick()
if group.type == "weight" then
assert.same({ min = 99, max = 101 }, encodedQuery.stats[1].value)
else
assert.same(group, encodedQuery.stats[1])
end
assert.same({ input = "ExampleAccount" }, encodedQuery.filters.trade_filters.filters.account)
end)
end
end)

it("fits the OAuth clipboard status inside the login button", function()
local status = mock_tradeQuery:FormatOAuthLoginStatus(60)

Expand Down
31 changes: 14 additions & 17 deletions src/Classes/CompareBuySimilar.lua
Original file line number Diff line number Diff line change
Expand Up @@ -58,25 +58,22 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is

-- Build query
local queryTable = {
query = {
status = { option = listedApiValue },
stats = {
{
type = "and",
filters = {}
}
},
status = { option = listedApiValue },
stats = {
{
type = "and",
filters = {}
}
},
sort = { price = "asc" }
}
local queryFilters = {}

if isUnique then
-- Search by unique name
-- Strip "Foulborn" prefix from unique name for trade search
local tradeName = (item.title or item.name):gsub("^Foulborn%s+", "")
queryTable.query.name = tradeName
queryTable.query.type = item.baseName
queryTable.name = tradeName
queryTable.type = item.baseName
-- If item is Foulborn, add the foulborn_item filter
if item.foulborn then
queryFilters.misc_filters = queryFilters.misc_filters or { filters = {} }
Expand All @@ -95,7 +92,7 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is

-- Base type filter
if controls.baseTypeCheck and controls.baseTypeCheck.state then
queryTable.query.type = item.baseName
queryTable.type = item.baseName
end

-- Item level filter
Expand Down Expand Up @@ -165,21 +162,21 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is
if controls[prefix .. "Check"] and controls[prefix .. "Check"].state then
if #entry.tradeIds == 1 then
-- 1 id entries are added to the stat filters section
t_insert(queryTable.query.stats[1].filters, getFilter(entry.tradeIds[1]))
t_insert(queryTable.stats[1].filters, getFilter(entry.tradeIds[1]))
elseif #entry.tradeIds > 1 then
-- ambiguous entries are added as a separate count filter
local countFilter = { type = "count", value = { min = 1 }, filters = {} }
for _, tradeId in ipairs(entry.tradeIds) do
t_insert(countFilter.filters, getFilter(tradeId))
end
t_insert(queryTable.query.stats, countFilter)
t_insert(queryTable.stats, countFilter)
end
end
end

-- Only include filters if we have any
if next(queryFilters) then
queryTable.query.filters = queryFilters
queryTable.filters = queryFilters
end

-- Build URL
Expand All @@ -191,8 +188,8 @@ local function buildURL(item, slotName, controls, modEntries, defenceEntries, is
local encodedLeague = league:gsub("[^%w%-%.%_%~]", function(c)
return string.format("%%%02X", string.byte(c))
end):gsub(" ", "+")
url = url .. "/" .. encodedLeague
url = url .. "?q=" .. urlEncode(queryJson)
url ..= "/" .. encodedLeague
url ..= "/" .. tradeHelpers.B64GzipEncode(queryJson)

return url
end
Expand Down
19 changes: 19 additions & 0 deletions src/Classes/TradeHelpers.lua
Original file line number Diff line number Diff line change
Expand Up @@ -596,4 +596,23 @@ function M.newPlainNumericEdit(anchor, rect, init, prompt, limit, integer, chang
end
return ctrl
end


---@param str string String which will be encoded
---@return string? result The given string, gzipped and then Base64URL encoded
function M.B64GzipEncode(str)
local b64 = require("base64")
local deflated = Deflate(str, true)
if not deflated then return end
return b64.encode(deflated):gsub("%+", "-"):gsub("/", "_")
end

---@param str string String which will be decoded
---@return string? result The given string, Base64URL decoded and the ungzipped
function M.B64GzipDecode(str)
local b64 = require("base64")
local data = b64.decode(str:gsub("%-", "+"):gsub("_", "/"))
if not data then return end
return Inflate(data)
end
return M
Loading
Loading