From ded24021aee817ad7b93b3317f776e24a8c1dbd2 Mon Sep 17 00:00:00 2001 From: Nightblade Date: Fri, 10 Apr 2026 11:35:36 +1000 Subject: [PATCH] space->tab indents --- spec/System/TestTradeQueryCurrency_spec.lua | 116 +++--- spec/System/TestTradeQueryGenerator_spec.lua | 108 +++--- .../System/TestTradeQueryRateLimiter_spec.lua | 142 +++---- spec/System/TestTradeQueryRequests_spec.lua | 364 +++++++++--------- 4 files changed, 365 insertions(+), 365 deletions(-) diff --git a/spec/System/TestTradeQueryCurrency_spec.lua b/spec/System/TestTradeQueryCurrency_spec.lua index 48b52f6f8a..d3cccb5298 100644 --- a/spec/System/TestTradeQueryCurrency_spec.lua +++ b/spec/System/TestTradeQueryCurrency_spec.lua @@ -1,65 +1,65 @@ describe("TradeQuery Currency Conversion", function() - local mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) + local mock_tradeQuery = new("TradeQuery", { itemsTab = {} }) - -- test case for commit: "Skip callback on errors to prevent incomplete conversions" - describe("FetchCurrencyConversionTable", function() - -- Pass: Callback not called on error - -- Fail: Callback called, indicating partial data risk - it("skips callback on error", function() - local orig_launch = launch - local spy = { called = false } - launch = { - DownloadPage = function(url, callback, opts) - callback(nil, "test error") - end - } - mock_tradeQuery:FetchCurrencyConversionTable(function() - spy.called = true - end) - launch = orig_launch - assert.is_false(spy.called) - end) - end) + -- test case for commit: "Skip callback on errors to prevent incomplete conversions" + describe("FetchCurrencyConversionTable", function() + -- Pass: Callback not called on error + -- Fail: Callback called, indicating partial data risk + it("skips callback on error", function() + local orig_launch = launch + local spy = { called = false } + launch = { + DownloadPage = function(url, callback, opts) + callback(nil, "test error") + end + } + mock_tradeQuery:FetchCurrencyConversionTable(function() + spy.called = true + end) + launch = orig_launch + assert.is_false(spy.called) + end) + end) - describe("ConvertCurrencyToChaos", function() - -- Pass: Ceils amount to integer (e.g., 4.9 -> 5) - -- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals - it("handles chaos currency", function() - mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } } - mock_tradeQuery.pbLeague = "league" - local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9) - assert.are.equal(result, 5) - end) + describe("ConvertCurrencyToChaos", function() + -- Pass: Ceils amount to integer (e.g., 4.9 -> 5) + -- Fail: Wrong value or nil, indicating broken rounding/baseline logic, causing inaccurate chaos totals + it("handles chaos currency", function() + mock_tradeQuery.pbCurrencyConversion = { league = { chaos = 1 } } + mock_tradeQuery.pbLeague = "league" + local result = mock_tradeQuery:ConvertCurrencyToChaos("chaos", 4.9) + assert.are.equal(result, 5) + end) - -- Pass: Returns nil without crash - -- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions - it("returns nil for unmapped", function() - local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10) - assert.is_nil(result) - end) - end) + -- Pass: Returns nil without crash + -- Fail: Crashes or wrong value, indicating unhandled currencies, corrupting price conversions + it("returns nil for unmapped", function() + local result = mock_tradeQuery:ConvertCurrencyToChaos("exotic", 10) + assert.is_nil(result) + end) + end) - describe("PriceBuilderProcessPoENinjaResponse", function() - -- Pass: Processes without error, restoring map - -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions - it("handles unmapped currency", function() - local orig_conv = mock_tradeQuery.currencyConversionTradeMap - mock_tradeQuery.currencyConversionTradeMap = { div = "id" } - local resp = { exotic = 10 } - mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp) - -- No crash expected - assert.is_true(true) - mock_tradeQuery.currencyConversionTradeMap = orig_conv - end) - end) + describe("PriceBuilderProcessPoENinjaResponse", function() + -- Pass: Processes without error, restoring map + -- Fail: Corrupts map or crashes, indicating fragile API response handling, breaking future conversions + it("handles unmapped currency", function() + local orig_conv = mock_tradeQuery.currencyConversionTradeMap + mock_tradeQuery.currencyConversionTradeMap = { div = "id" } + local resp = { exotic = 10 } + mock_tradeQuery:PriceBuilderProcessPoENinjaResponse(resp) + -- No crash expected + assert.is_true(true) + mock_tradeQuery.currencyConversionTradeMap = orig_conv + end) + end) - describe("GetTotalPriceString", function() - -- Pass: Sums and formats correctly (e.g., "5 chaos, 10 div") - -- Fail: Wrong string (e.g., unsorted/missing sums), indicating aggregation bug, misleading users on totals - it("aggregates prices", function() - mock_tradeQuery.totalPrice = { { currency = "chaos", amount = 5 }, { currency = "div", amount = 10 } } - local result = mock_tradeQuery:GetTotalPriceString() - assert.are.equal(result, "5 chaos, 10 div") - end) - end) + describe("GetTotalPriceString", function() + -- Pass: Sums and formats correctly (e.g., "5 chaos, 10 div") + -- Fail: Wrong string (e.g., unsorted/missing sums), indicating aggregation bug, misleading users on totals + it("aggregates prices", function() + mock_tradeQuery.totalPrice = { { currency = "chaos", amount = 5 }, { currency = "div", amount = 10 } } + local result = mock_tradeQuery:GetTotalPriceString() + assert.are.equal(result, "5 chaos, 10 div") + end) + end) end) diff --git a/spec/System/TestTradeQueryGenerator_spec.lua b/spec/System/TestTradeQueryGenerator_spec.lua index e8e93774ca..befb96a657 100644 --- a/spec/System/TestTradeQueryGenerator_spec.lua +++ b/spec/System/TestTradeQueryGenerator_spec.lua @@ -1,60 +1,60 @@ describe("TradeQueryGenerator", function() - local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) + local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) - describe("ProcessMod", function() - -- Pass: Mod line maps correctly to trade stat entry without error - -- Fail: Mapping fails (e.g., no match found), indicating incomplete stat parsing for curse mods, potentially missing curse-enabling items in queries - it("handles special curse case", function() - local mod = { "You can apply an additional Curse" } - local tradeStatsParsed = { result = { [2] = { entries = { { text = "You can apply # additional Curses", id = "id" } } } } } - mock_queryGen.modData = { Explicit = true } - mock_queryGen:ProcessMod(mod, tradeStatsParsed, 1) - -- Simplified assertion; in full impl, check modData - assert.is_true(true) - end) - end) + describe("ProcessMod", function() + -- Pass: Mod line maps correctly to trade stat entry without error + -- Fail: Mapping fails (e.g., no match found), indicating incomplete stat parsing for curse mods, potentially missing curse-enabling items in queries + it("handles special curse case", function() + local mod = { "You can apply an additional Curse" } + local tradeStatsParsed = { result = { [2] = { entries = { { text = "You can apply # additional Curses", id = "id" } } } } } + mock_queryGen.modData = { Explicit = true } + mock_queryGen:ProcessMod(mod, tradeStatsParsed, 1) + -- Simplified assertion; in full impl, check modData + assert.is_true(true) + end) + end) - describe("WeightedRatioOutputs", function() - -- Pass: Returns 0, avoiding math errors - -- Fail: Returns NaN/inf or crashes, indicating unhandled infinite values, causing evaluation failures in infinite-scaling builds - it("handles infinite base", function() - local baseOutput = { TotalDPS = math.huge } - local newOutput = { TotalDPS = 100 } - local statWeights = { { stat = "TotalDPS", weightMult = 1 } } - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) - assert.are.equal(result, 0) - end) + describe("WeightedRatioOutputs", function() + -- Pass: Returns 0, avoiding math errors + -- Fail: Returns NaN/inf or crashes, indicating unhandled infinite values, causing evaluation failures in infinite-scaling builds + it("handles infinite base", function() + local baseOutput = { TotalDPS = math.huge } + local newOutput = { TotalDPS = 100 } + local statWeights = { { stat = "TotalDPS", weightMult = 1 } } + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + assert.are.equal(result, 0) + end) - -- Pass: Returns capped value (100), preventing division issues - -- Fail: Returns inf/NaN, indicating unhandled zero base, leading to invalid comparisons in low-output builds - it("handles zero base", function() - local baseOutput = { TotalDPS = 0 } - local newOutput = { TotalDPS = 100 } - local statWeights = { { stat = "TotalDPS", weightMult = 1 } } - data.misc.maxStatIncrease = 1000 - local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) - assert.are.equal(result, 100) - end) - end) + -- Pass: Returns capped value (100), preventing division issues + -- Fail: Returns inf/NaN, indicating unhandled zero base, leading to invalid comparisons in low-output builds + it("handles zero base", function() + local baseOutput = { TotalDPS = 0 } + local newOutput = { TotalDPS = 100 } + local statWeights = { { stat = "TotalDPS", weightMult = 1 } } + data.misc.maxStatIncrease = 1000 + local result = mock_queryGen.WeightedRatioOutputs(baseOutput, newOutput, statWeights) + assert.are.equal(result, 100) + end) + end) - describe("Filter prioritization", function() - -- Pass: Limits mods to MAX_FILTERS (2 in test), preserving top priorities - -- Fail: Exceeds limit, indicating over-generation of filters, risking API query size errors or rate limits - it("respects MAX_FILTERS", function() - local orig_max = _G.MAX_FILTERS - _G.MAX_FILTERS = 2 - mock_queryGen.modWeights = { { weight = 10, tradeModId = "id1" }, { weight = 5, tradeModId = "id2" } } - table.sort(mock_queryGen.modWeights, function(a, b) - return math.abs(a.weight) > math.abs(b.weight) - end) - local prioritized = {} - for i, entry in ipairs(mock_queryGen.modWeights) do - if #prioritized < _G.MAX_FILTERS then - table.insert(prioritized, entry) - end - end - assert.are.equal(#prioritized, 2) - _G.MAX_FILTERS = orig_max - end) - end) + describe("Filter prioritization", function() + -- Pass: Limits mods to MAX_FILTERS (2 in test), preserving top priorities + -- Fail: Exceeds limit, indicating over-generation of filters, risking API query size errors or rate limits + it("respects MAX_FILTERS", function() + local orig_max = _G.MAX_FILTERS + _G.MAX_FILTERS = 2 + mock_queryGen.modWeights = { { weight = 10, tradeModId = "id1" }, { weight = 5, tradeModId = "id2" } } + table.sort(mock_queryGen.modWeights, function(a, b) + return math.abs(a.weight) > math.abs(b.weight) + end) + local prioritized = {} + for i, entry in ipairs(mock_queryGen.modWeights) do + if #prioritized < _G.MAX_FILTERS then + table.insert(prioritized, entry) + end + end + assert.are.equal(#prioritized, 2) + _G.MAX_FILTERS = orig_max + end) + end) end) diff --git a/spec/System/TestTradeQueryRateLimiter_spec.lua b/spec/System/TestTradeQueryRateLimiter_spec.lua index 0fd4a09e0b..4542385fdf 100644 --- a/spec/System/TestTradeQueryRateLimiter_spec.lua +++ b/spec/System/TestTradeQueryRateLimiter_spec.lua @@ -1,78 +1,78 @@ describe("TradeQueryRateLimiter", function() - describe("ParseHeader", function() - -- Pass: Extracts keys/values correctly - -- Fail: Nil/malformed values, indicating regex failure, breaking policy updates from API - it("parses basic headers", function() - local limiter = new("TradeQueryRateLimiter") - local headers = limiter:ParseHeader("X-Rate-Limit-Policy: test\nRetry-After: 5\nContent-Type: json") - assert.are.equal(headers["x-rate-limit-policy"], "test") - assert.are.equal(headers["retry-after"], "5") - assert.are.equal(headers["content-type"], "json") - end) - end) + describe("ParseHeader", function() + -- Pass: Extracts keys/values correctly + -- Fail: Nil/malformed values, indicating regex failure, breaking policy updates from API + it("parses basic headers", function() + local limiter = new("TradeQueryRateLimiter") + local headers = limiter:ParseHeader("X-Rate-Limit-Policy: test\nRetry-After: 5\nContent-Type: json") + assert.are.equal(headers["x-rate-limit-policy"], "test") + assert.are.equal(headers["retry-after"], "5") + assert.are.equal(headers["content-type"], "json") + end) + end) - describe("ParsePolicy", function() - -- Pass: Extracts rules/limits/states accurately - -- Fail: Wrong buckets/windows, indicating parsing bug, enforcing incorrect rates - it("parses full policy", function() - local limiter = new("TradeQueryRateLimiter") - local header = "X-Rate-Limit-Policy: trade-search-request-limit\nX-Rate-Limit-Rules: Ip,Account\nX-Rate-Limit-Ip: 8:10:60,15:60:120\nX-Rate-Limit-Ip-State: 7:10:60,14:60:120\nX-Rate-Limit-Account: 2:5:60\nX-Rate-Limit-Account-State: 1:5:60\nRetry-After: 10" - local policies = limiter:ParsePolicy(header) - local policy = policies["trade-search-request-limit"] - assert.are.equal(policy.ip.limits[10].request, 8) - assert.are.equal(policy.ip.limits[10].timeout, 60) - assert.are.equal(policy.ip.state[10].request, 7) - assert.are.equal(policy.account.limits[5].request, 2) - end) - end) + describe("ParsePolicy", function() + -- Pass: Extracts rules/limits/states accurately + -- Fail: Wrong buckets/windows, indicating parsing bug, enforcing incorrect rates + it("parses full policy", function() + local limiter = new("TradeQueryRateLimiter") + local header = "X-Rate-Limit-Policy: trade-search-request-limit\nX-Rate-Limit-Rules: Ip,Account\nX-Rate-Limit-Ip: 8:10:60,15:60:120\nX-Rate-Limit-Ip-State: 7:10:60,14:60:120\nX-Rate-Limit-Account: 2:5:60\nX-Rate-Limit-Account-State: 1:5:60\nRetry-After: 10" + local policies = limiter:ParsePolicy(header) + local policy = policies["trade-search-request-limit"] + assert.are.equal(policy.ip.limits[10].request, 8) + assert.are.equal(policy.ip.limits[10].timeout, 60) + assert.are.equal(policy.ip.state[10].request, 7) + assert.are.equal(policy.account.limits[5].request, 2) + end) + end) - describe("UpdateFromHeader", function() - -- Pass: Reduces limits (e.g., 5 -> 4) - -- Fail: Unchanged limits, indicating margin ignored, risking user over-requests - it("applies margin to limits", function() - local limiter = new("TradeQueryRateLimiter") - limiter.limitMargin = 1 - local header = "X-Rate-Limit-Policy: test\nX-Rate-Limit-Rules: Ip\nX-Rate-Limit-Ip: 5:10:60\nX-Rate-Limit-Ip-State: 4:10:60" - limiter:UpdateFromHeader(header) - assert.are.equal(limiter.policies["test"].ip.limits[10].request, 4) - end) - end) + describe("UpdateFromHeader", function() + -- Pass: Reduces limits (e.g., 5 -> 4) + -- Fail: Unchanged limits, indicating margin ignored, risking user over-requests + it("applies margin to limits", function() + local limiter = new("TradeQueryRateLimiter") + limiter.limitMargin = 1 + local header = "X-Rate-Limit-Policy: test\nX-Rate-Limit-Rules: Ip\nX-Rate-Limit-Ip: 5:10:60\nX-Rate-Limit-Ip-State: 4:10:60" + limiter:UpdateFromHeader(header) + assert.are.equal(limiter.policies["test"].ip.limits[10].request, 4) + end) + end) - describe("NextRequestTime", function() - -- Pass: Delays past timestamp - -- Fail: Allows immediate request, indicating ignored cooldowns, causing 429 errors - it("blocks on retry-after", function() - local limiter = new("TradeQueryRateLimiter") - local now = os.time() - limiter.policies["test"] = {} - limiter.retryAfter["test"] = now + 10 - local nextTime = limiter:NextRequestTime("test", now) - assert.is_true(nextTime > now) - end) + describe("NextRequestTime", function() + -- Pass: Delays past timestamp + -- Fail: Allows immediate request, indicating ignored cooldowns, causing 429 errors + it("blocks on retry-after", function() + local limiter = new("TradeQueryRateLimiter") + local now = os.time() + limiter.policies["test"] = {} + limiter.retryAfter["test"] = now + 10 + local nextTime = limiter:NextRequestTime("test", now) + assert.is_true(nextTime > now) + end) - -- Pass: Calculates delay from timestamps - -- Fail: Allows request in limit, indicating state misread, over-throttling or bans - it("blocks on window limit", function() - local limiter = new("TradeQueryRateLimiter") - local now = os.time() - limiter.policies["test"] = { ["ip"] = { ["limits"] = { ["10"] = { ["request"] = 1, ["timeout"] = 60 } }, ["state"] = { ["10"] = { ["request"] = 1, ["timeout"] = 0 } } } } - limiter.requestHistory["test"] = { timestamps = {now - 5} } - limiter.lastUpdate["test"] = now - 5 - local nextTime = limiter:NextRequestTime("test", now) - assert.is_true(nextTime > now) - end) - end) + -- Pass: Calculates delay from timestamps + -- Fail: Allows request in limit, indicating state misread, over-throttling or bans + it("blocks on window limit", function() + local limiter = new("TradeQueryRateLimiter") + local now = os.time() + limiter.policies["test"] = { ["ip"] = { ["limits"] = { ["10"] = { ["request"] = 1, ["timeout"] = 60 } }, ["state"] = { ["10"] = { ["request"] = 1, ["timeout"] = 0 } } } } + limiter.requestHistory["test"] = { timestamps = {now - 5} } + limiter.lastUpdate["test"] = now - 5 + local nextTime = limiter:NextRequestTime("test", now) + assert.is_true(nextTime > now) + end) + end) - describe("AgeOutRequests", function() - -- Pass: Removes old stamps, decrements to 1 - -- Fail: Stale data persists, indicating aging bug, perpetual blocking - it("cleans up timestamps and decrements", function() - local limiter = new("TradeQueryRateLimiter") - limiter.policies["test"] = { ["ip"] = { ["state"] = { ["10"] = { ["request"] = 2, ["timeout"] = 0, ["decremented"] = nil } } } } - limiter.requestHistory["test"] = { timestamps = {os.time() - 15, os.time() - 5}, maxWindow=10, lastCheck=os.time() - 10 } - limiter:AgeOutRequests("test", os.time()) - assert.are.equal(limiter.policies["test"].ip.state["10"].request, 1) - assert.are.equal(#limiter.requestHistory["test"].timestamps, 1) - end) - end) + describe("AgeOutRequests", function() + -- Pass: Removes old stamps, decrements to 1 + -- Fail: Stale data persists, indicating aging bug, perpetual blocking + it("cleans up timestamps and decrements", function() + local limiter = new("TradeQueryRateLimiter") + limiter.policies["test"] = { ["ip"] = { ["state"] = { ["10"] = { ["request"] = 2, ["timeout"] = 0, ["decremented"] = nil } } } } + limiter.requestHistory["test"] = { timestamps = {os.time() - 15, os.time() - 5}, maxWindow=10, lastCheck=os.time() - 10 } + limiter:AgeOutRequests("test", os.time()) + assert.are.equal(limiter.policies["test"].ip.state["10"].request, 1) + assert.are.equal(#limiter.requestHistory["test"].timestamps, 1) + end) + end) end) diff --git a/spec/System/TestTradeQueryRequests_spec.lua b/spec/System/TestTradeQueryRequests_spec.lua index 6e0c7658e5..873b5102d5 100644 --- a/spec/System/TestTradeQueryRequests_spec.lua +++ b/spec/System/TestTradeQueryRequests_spec.lua @@ -1,195 +1,195 @@ describe("TradeQueryRequests", function() - local mock_limiter = { - NextRequestTime = function() - return os.time() - end, - InsertRequest = function() - return 1 - end, - FinishRequest = function() end, - UpdateFromHeader = function() end, - GetPolicyName = function(self, key) - return key - end - } - local requests = new("TradeQueryRequests", mock_limiter) + local mock_limiter = { + NextRequestTime = function() + return os.time() + end, + InsertRequest = function() + return 1 + end, + FinishRequest = function() end, + UpdateFromHeader = function() end, + GetPolicyName = function(self, key) + return key + end + } + local requests = new("TradeQueryRequests", mock_limiter) - local function simulateRetry(requests, mock_limiter, policy, current_time) - local now = current_time - local queue = requests.requestQueue.search - local request = table.remove(queue, 1) - local requestId = mock_limiter:InsertRequest(policy) - local response = { header = "HTTP/1.1 429 Too Many Requests" } - mock_limiter:FinishRequest(policy, requestId) - mock_limiter:UpdateFromHeader(response.header) - local status = response.header:match("HTTP/[%d%%%.]+ (%d+)") - if status == "429" then - request.attempts = (request.attempts or 0) + 1 - local backoff = math.min(2 ^ request.attempts, 60) - request.retryTime = now + backoff - table.insert(queue, 1, request) - return true, request.attempts, request.retryTime - end - return false, nil, nil - end + local function simulateRetry(requests, mock_limiter, policy, current_time) + local now = current_time + local queue = requests.requestQueue.search + local request = table.remove(queue, 1) + local requestId = mock_limiter:InsertRequest(policy) + local response = { header = "HTTP/1.1 429 Too Many Requests" } + mock_limiter:FinishRequest(policy, requestId) + mock_limiter:UpdateFromHeader(response.header) + local status = response.header:match("HTTP/[%d%%%.]+ (%d+)") + if status == "429" then + request.attempts = (request.attempts or 0) + 1 + local backoff = math.min(2 ^ request.attempts, 60) + request.retryTime = now + backoff + table.insert(queue, 1, request) + return true, request.attempts, request.retryTime + end + return false, nil, nil + end - describe("ProcessQueue", function() - -- Pass: No changes to empty queues - -- Fail: Alters queues unexpectedly, indicating loop errors, causing phantom requests - it("skips empty queue", function() - requests.requestQueue = { search = {}, fetch = {} } - requests:ProcessQueue() - assert.are.equal(#requests.requestQueue.search, 0) - end) + describe("ProcessQueue", function() + -- Pass: No changes to empty queues + -- Fail: Alters queues unexpectedly, indicating loop errors, causing phantom requests + it("skips empty queue", function() + requests.requestQueue = { search = {}, fetch = {} } + requests:ProcessQueue() + assert.are.equal(#requests.requestQueue.search, 0) + end) - -- Pass: Dequeues and processes valid item - -- Fail: Queue unchanged, indicating timing/insertion bug, blocking trade searches - it("processes search queue item", function() - local orig_launch = launch - launch = { - DownloadPage = function(url, onComplete, opts) - onComplete({ body = "{}", header = "HTTP/1.1 200 OK" }, nil) - end - } - table.insert(requests.requestQueue.search, { - url = "test", - callback = function() end, - retryTime = nil - }) - local function mock_next_time(self, policy, time) - return time - 1 - end - mock_limiter.NextRequestTime = mock_next_time - requests:ProcessQueue() - assert.are.equal(#requests.requestQueue.search, 0) - launch = orig_launch - end) + -- Pass: Dequeues and processes valid item + -- Fail: Queue unchanged, indicating timing/insertion bug, blocking trade searches + it("processes search queue item", function() + local orig_launch = launch + launch = { + DownloadPage = function(url, onComplete, opts) + onComplete({ body = "{}", header = "HTTP/1.1 200 OK" }, nil) + end + } + table.insert(requests.requestQueue.search, { + url = "test", + callback = function() end, + retryTime = nil + }) + local function mock_next_time(self, policy, time) + return time - 1 + end + mock_limiter.NextRequestTime = mock_next_time + requests:ProcessQueue() + assert.are.equal(#requests.requestQueue.search, 0) + launch = orig_launch + end) - -- Pass: Retries with increasing backoff up to cap, preventing infinite loops - -- Fail: No backoff or uncapped, indicating retry bug, risking API bans - it("retries on 429 with exponential backoff", function() - local orig_os_time = os.time - local mock_time = 1000 - os.time = function() return mock_time end + -- Pass: Retries with increasing backoff up to cap, preventing infinite loops + -- Fail: No backoff or uncapped, indicating retry bug, risking API bans + it("retries on 429 with exponential backoff", function() + local orig_os_time = os.time + local mock_time = 1000 + os.time = function() return mock_time end - local request = { - url = "test", - callback = function() end, - retryTime = nil, - attempts = 0 - } - table.insert(requests.requestQueue.search, request) + local request = { + url = "test", + callback = function() end, + retryTime = nil, + attempts = 0 + } + table.insert(requests.requestQueue.search, request) - local policy = mock_limiter:GetPolicyName("search") + local policy = mock_limiter:GetPolicyName("search") - for i = 1, 7 do - local previous_time = mock_time - local entered, attempts, retryTime = simulateRetry(requests, mock_limiter, policy, mock_time) - assert.is_true(entered) - assert.are.equal(attempts, i) - local expected_backoff = math.min(math.pow(2, i), 60) - assert.are.equal(retryTime, previous_time + expected_backoff) - mock_time = retryTime - end + for i = 1, 7 do + local previous_time = mock_time + local entered, attempts, retryTime = simulateRetry(requests, mock_limiter, policy, mock_time) + assert.is_true(entered) + assert.are.equal(attempts, i) + local expected_backoff = math.min(math.pow(2, i), 60) + assert.are.equal(retryTime, previous_time + expected_backoff) + mock_time = retryTime + end - -- Validate skip when time < retryTime - mock_time = requests.requestQueue.search[1].retryTime - 1 - local function mock_next_time(self, policy, time) - return time - 1 - end - mock_limiter.NextRequestTime = mock_next_time - requests:ProcessQueue() - assert.are.equal(#requests.requestQueue.search, 1) + -- Validate skip when time < retryTime + mock_time = requests.requestQueue.search[1].retryTime - 1 + local function mock_next_time(self, policy, time) + return time - 1 + end + mock_limiter.NextRequestTime = mock_next_time + requests:ProcessQueue() + assert.are.equal(#requests.requestQueue.search, 1) - os.time = orig_os_time - end) - end) + os.time = orig_os_time + end) + end) - describe("SearchWithQueryWeightAdjusted", function() - -- Pass: Caps at 5 calls on large results - -- Fail: Exceeds 5, indicating loop without bound, risking stack overflow or endless API calls - it("respects recursion limit", function() - local call_count = 0 - local orig_perform = requests.PerformSearch - local orig_fetchBlock = requests.FetchResultBlock - local valid_query = [[{"query":{"stats":[{"value":{"min":0}}]}}]] - local test_ids = {} - for i = 1, 11 do - table.insert(test_ids, "item" .. i) - end - requests.PerformSearch = function(self, realm, league, query, callback) - call_count = call_count + 1 - local response - if call_count >= 5 then - response = { total = 11, result = test_ids, id = "id" } - else - response = { total = 10000, result = { "item1" }, id = "id" } - end - callback(response, nil) - end - requests.FetchResultBlock = function(self, url, callback) - local param_item_hashes = url:match("fetch/([^?]+)") - local hashes = {} - if param_item_hashes then - for hash in param_item_hashes:gmatch("[^,]+") do - table.insert(hashes, hash) - end - end - local processedItems = {} - for _, hash in ipairs(hashes) do - table.insert(processedItems, { - amount = 1, - currency = "chaos", - item_string = "Test Item", - whisper = "hi", - weight = "100", - id = hash - }) - end - callback(processedItems) - end - requests:SearchWithQueryWeightAdjusted("pc", "league", valid_query, function(items) - assert.are.equal(call_count, 5) - end, {}) - requests.PerformSearch = orig_perform - requests.FetchResultBlock = orig_fetchBlock - end) - end) + describe("SearchWithQueryWeightAdjusted", function() + -- Pass: Caps at 5 calls on large results + -- Fail: Exceeds 5, indicating loop without bound, risking stack overflow or endless API calls + it("respects recursion limit", function() + local call_count = 0 + local orig_perform = requests.PerformSearch + local orig_fetchBlock = requests.FetchResultBlock + local valid_query = [[{"query":{"stats":[{"value":{"min":0}}]}}]] + local test_ids = {} + for i = 1, 11 do + table.insert(test_ids, "item" .. i) + end + requests.PerformSearch = function(self, realm, league, query, callback) + call_count = call_count + 1 + local response + if call_count >= 5 then + response = { total = 11, result = test_ids, id = "id" } + else + response = { total = 10000, result = { "item1" }, id = "id" } + end + callback(response, nil) + end + requests.FetchResultBlock = function(self, url, callback) + local param_item_hashes = url:match("fetch/([^?]+)") + local hashes = {} + if param_item_hashes then + for hash in param_item_hashes:gmatch("[^,]+") do + table.insert(hashes, hash) + end + end + local processedItems = {} + for _, hash in ipairs(hashes) do + table.insert(processedItems, { + amount = 1, + currency = "chaos", + item_string = "Test Item", + whisper = "hi", + weight = "100", + id = hash + }) + end + callback(processedItems) + end + requests:SearchWithQueryWeightAdjusted("pc", "league", valid_query, function(items) + assert.are.equal(call_count, 5) + end, {}) + requests.PerformSearch = orig_perform + requests.FetchResultBlock = orig_fetchBlock + 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 - it("fetches up to maxFetchPerSearch items", function() - local itemHashes = { "id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10", "id11" } - local block_count = 0 - local orig_fetchBlock = requests.FetchResultBlock - requests.FetchResultBlock = function(self, url, callback) - block_count = block_count + 1 - local param_item_hashes = url:match("fetch/([^?]+)") - local hashes = {} - if param_item_hashes then - for hash in param_item_hashes:gmatch("[^,]+") do - table.insert(hashes, hash) - end - end - local processedItems = {} - for _, hash in ipairs(hashes) do - table.insert(processedItems, { - amount = 1, - currency = "chaos", - item_string = "Test Item", - whisper = "hi", - weight = "100", - id = hash - }) - end - callback(processedItems) - end - requests:FetchResults(itemHashes, "queryId", function(items) - assert.are.equal(#items, 10) - assert.are.equal(block_count, 1) - end) - requests.FetchResultBlock = orig_fetchBlock - 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 + it("fetches up to maxFetchPerSearch items", function() + local itemHashes = { "id1", "id2", "id3", "id4", "id5", "id6", "id7", "id8", "id9", "id10", "id11" } + local block_count = 0 + local orig_fetchBlock = requests.FetchResultBlock + requests.FetchResultBlock = function(self, url, callback) + block_count = block_count + 1 + local param_item_hashes = url:match("fetch/([^?]+)") + local hashes = {} + if param_item_hashes then + for hash in param_item_hashes:gmatch("[^,]+") do + table.insert(hashes, hash) + end + end + local processedItems = {} + for _, hash in ipairs(hashes) do + table.insert(processedItems, { + amount = 1, + currency = "chaos", + item_string = "Test Item", + whisper = "hi", + weight = "100", + id = hash + }) + end + callback(processedItems) + end + requests:FetchResults(itemHashes, "queryId", function(items) + assert.are.equal(#items, 10) + assert.are.equal(block_count, 1) + end) + requests.FetchResultBlock = orig_fetchBlock + end) + end) end) \ No newline at end of file