|
| 1 | +local dkjson = require "dkjson" |
| 2 | + |
1 | 3 | describe("TradeQueryGenerator", function() |
2 | | - local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {} }) |
| 4 | + local mock_queryGen = new("TradeQueryGenerator", { itemsTab = {}, GetTradeStatusOption = function() return "online" end }) |
3 | 5 |
|
4 | 6 | describe("ProcessMod", function() |
5 | 7 | -- Pass: Mod line maps correctly to trade stat entry without error |
@@ -57,4 +59,261 @@ describe("TradeQueryGenerator", function() |
57 | 59 | _G.MAX_FILTERS = orig_max |
58 | 60 | end) |
59 | 61 | end) |
| 62 | + |
| 63 | + describe("Required mod filters", function() |
| 64 | + -- Synthetic trade IDs used to isolate tests from the live trade stats lookup. |
| 65 | + local CORRUPTED_BLOOD_ID = "corrupted.stat_3299347024" |
| 66 | + local MAX_LIFE_ID = "implicit.stat_3299347025" |
| 67 | + |
| 68 | + local function makeQueryGen() |
| 69 | + local qg = new("TradeQueryGenerator", { itemsTab = { items = {} } }) |
| 70 | + -- Inject known trade ID mappings keyed by mod type; avoids dependency on live API data. |
| 71 | + qg.modTradeIdByText = { |
| 72 | + Corrupted = { |
| 73 | + ["Corrupted Blood cannot be inflicted on you"] = CORRUPTED_BLOOD_ID, |
| 74 | + }, |
| 75 | + Implicit = { |
| 76 | + [" to maximum Life"] = MAX_LIFE_ID, -- normalized form of "+# to maximum Life" |
| 77 | + }, |
| 78 | + } |
| 79 | + qg.requesterContext = {} |
| 80 | + return qg |
| 81 | + end |
| 82 | + |
| 83 | + local function baseCalcContext(pinnedModLines) |
| 84 | + return { |
| 85 | + slot = { selItemId = 1, slotName = "Jewel 1" }, |
| 86 | + testItem = new("Item", "Rarity: RARE\nStat Tester\nCrimson Jewel"), |
| 87 | + calcFunc = function() return { Score = 0 } end, |
| 88 | + baseOutput = { Score = 0 }, |
| 89 | + baseStatValue = 0, |
| 90 | + itemCategoryQueryStr = "jewel", |
| 91 | + options = { |
| 92 | + includeMirrored = true, |
| 93 | + influence1 = 1, |
| 94 | + influence2 = 1, |
| 95 | + statWeights = {}, |
| 96 | + pinnedModLines = pinnedModLines or {}, |
| 97 | + }, |
| 98 | + special = {}, |
| 99 | + } |
| 100 | + end |
| 101 | + |
| 102 | + local function equippedItem() |
| 103 | + return { |
| 104 | + explicitModLines = {}, |
| 105 | + scourgeModLines = {}, |
| 106 | + implicitModLines = {}, |
| 107 | + crucibleModLines = {}, |
| 108 | + } |
| 109 | + end |
| 110 | + |
| 111 | + describe("ResolveRequiredModFilters", function() |
| 112 | + -- Pass: No mods pinned => empty list, existing query unchanged. |
| 113 | + -- Fail: Returns non-empty list, inserting spurious and-filters into the query. |
| 114 | + it("returns empty list when pinnedModLines is empty", function() |
| 115 | + local qg = makeQueryGen() |
| 116 | + local result = qg:ResolveRequiredModFilters({}) |
| 117 | + assert.are.same({}, result) |
| 118 | + end) |
| 119 | + |
| 120 | + -- Pass: nil input treated as no mods pinned. |
| 121 | + -- Fail: Crashes on nil, breaking queries that never set pinnedModLines. |
| 122 | + it("returns empty list when pinnedModLines is nil", function() |
| 123 | + local qg = makeQueryGen() |
| 124 | + local result = qg:ResolveRequiredModFilters(nil) |
| 125 | + assert.are.same({}, result) |
| 126 | + end) |
| 127 | + |
| 128 | + -- Pass: Single mod resolves to the correct trade stat ID. |
| 129 | + -- Fail: Wrong ID or empty list, causing the filter to be missing from the query. |
| 130 | + it("resolves a single required mod to its trade filter id", function() |
| 131 | + local qg = makeQueryGen() |
| 132 | + local result = qg:ResolveRequiredModFilters({ "Corrupted Blood cannot be inflicted on you" }) |
| 133 | + assert.are.equal(1, #result) |
| 134 | + assert.are.equal(CORRUPTED_BLOOD_ID, result[1].id) |
| 135 | + end) |
| 136 | + |
| 137 | + -- Pass: Corrupted Blood specifically maps to a valid trade stat id. |
| 138 | + -- Fail: Returns nil/empty, meaning the immunity would not be enforced in trade queries. |
| 139 | + it("handles 'Corrupted Blood cannot be inflicted on you' mod", function() |
| 140 | + local qg = makeQueryGen() |
| 141 | + local result = qg:ResolveRequiredModFilters({ "Corrupted Blood cannot be inflicted on you" }) |
| 142 | + assert.are.equal(1, #result) |
| 143 | + assert.is_not_nil(result[1].id) |
| 144 | + assert.are.equal(CORRUPTED_BLOOD_ID, result[1].id) |
| 145 | + end) |
| 146 | + |
| 147 | + -- Pass: Two mods each produce their own filter entry. |
| 148 | + -- Fail: Only one entry returned, silently dropping a required constraint. |
| 149 | + it("resolves multiple required mods to separate filter entries", function() |
| 150 | + local qg = makeQueryGen() |
| 151 | + local result = qg:ResolveRequiredModFilters({ |
| 152 | + "Corrupted Blood cannot be inflicted on you", |
| 153 | + "+42 to maximum Life", |
| 154 | + }) |
| 155 | + assert.are.equal(2, #result) |
| 156 | + assert.are.equal(CORRUPTED_BLOOD_ID, result[1].id) |
| 157 | + assert.are.equal(MAX_LIFE_ID, result[2].id) |
| 158 | + end) |
| 159 | + |
| 160 | + -- Pass: Implicit mod resolves to implicit trade ID, not explicit. |
| 161 | + -- Fail: Returns explicit ID, causing trade API to search wrong mod category. |
| 162 | + it("resolves implicit mod to implicit trade ID, not explicit", function() |
| 163 | + local qg = new("TradeQueryGenerator", { itemsTab = { items = {} } }) |
| 164 | + qg.modTradeIdByText = { |
| 165 | + Explicit = { |
| 166 | + [" to maximum Life"] = "explicit.stat_3299347025", |
| 167 | + }, |
| 168 | + Implicit = { |
| 169 | + [" to maximum Life"] = "implicit.stat_3299347025", |
| 170 | + }, |
| 171 | + } |
| 172 | + qg.requesterContext = {} |
| 173 | + local result = qg:ResolveRequiredModFilters({ "+42 to maximum Life" }) |
| 174 | + assert.are.equal(1, #result) |
| 175 | + assert.are.equal("implicit.stat_3299347025", result[1].id) |
| 176 | + end) |
| 177 | + |
| 178 | + -- Pass: Unknown mod is skipped; function returns empty without crashing. |
| 179 | + -- Fail: Crashes or raises an error, breaking the query flow entirely. |
| 180 | + it("skips mods with no trade mapping without crashing", function() |
| 181 | + local qg = makeQueryGen() |
| 182 | + local ok, result = pcall(function() |
| 183 | + return qg:ResolveRequiredModFilters({ "This mod does not exist in trade data" }) |
| 184 | + end) |
| 185 | + assert.is_true(ok) |
| 186 | + assert.are.same({}, result) |
| 187 | + end) |
| 188 | + end) |
| 189 | + |
| 190 | + describe("FinishQuery with pinnedModLines", function() |
| 191 | + -- Pass: No pinned mods => query has only the weighted stat group, no and-group. |
| 192 | + -- Fail: Spurious and-group inserted, altering previously working queries. |
| 193 | + it("generates query without required mods: no and-group added", function() |
| 194 | + local queryJson |
| 195 | + local originalClosePopup = main.ClosePopup |
| 196 | + main.ClosePopup = function() end |
| 197 | + |
| 198 | + local qg = makeQueryGen() |
| 199 | + qg.requesterCallback = function(_, payload) queryJson = payload end |
| 200 | + qg.modWeights = { { tradeModId = "explicit.stat_1", weight = 10, meanStatDiff = 100 } } |
| 201 | + qg.calcContext = baseCalcContext(nil) |
| 202 | + qg.itemsTab.items[1] = equippedItem() |
| 203 | + |
| 204 | + qg:FinishQuery() |
| 205 | + main.ClosePopup = originalClosePopup |
| 206 | + |
| 207 | + local query = dkjson.decode(queryJson) |
| 208 | + assert.are.equal(1, #query.query.stats) |
| 209 | + assert.are.equal("weight", query.query.stats[1].type) |
| 210 | + end) |
| 211 | + |
| 212 | + -- Pass: Pinned mod appears as an and-filter in the generated query. |
| 213 | + -- Fail: No and-group in query, meaning the required mod constraint is silently dropped. |
| 214 | + it("adds a required mod as an and-filter in the query", function() |
| 215 | + local queryJson |
| 216 | + local originalClosePopup = main.ClosePopup |
| 217 | + main.ClosePopup = function() end |
| 218 | + |
| 219 | + local qg = makeQueryGen() |
| 220 | + qg.requesterCallback = function(_, payload) queryJson = payload end |
| 221 | + qg.modWeights = { { tradeModId = "explicit.stat_1", weight = 10, meanStatDiff = 100 } } |
| 222 | + qg.calcContext = baseCalcContext({ "Corrupted Blood cannot be inflicted on you" }) |
| 223 | + qg.itemsTab.items[1] = equippedItem() |
| 224 | + |
| 225 | + qg:FinishQuery() |
| 226 | + main.ClosePopup = originalClosePopup |
| 227 | + |
| 228 | + local query = dkjson.decode(queryJson) |
| 229 | + assert.are.equal(2, #query.query.stats) |
| 230 | + local andGroup = query.query.stats[2] |
| 231 | + assert.are.equal("and", andGroup.type) |
| 232 | + assert.are.equal(1, #andGroup.filters) |
| 233 | + assert.are.equal(CORRUPTED_BLOOD_ID, andGroup.filters[1].id) |
| 234 | + end) |
| 235 | + |
| 236 | + -- Pass: Corrupted Blood mod is enforced end-to-end in the trade query JSON. |
| 237 | + -- Fail: No and-group or wrong id, allowing trade results that lack the immunity. |
| 238 | + it("enforces 'Corrupted Blood cannot be inflicted on you' as a required filter", function() |
| 239 | + local queryJson |
| 240 | + local originalClosePopup = main.ClosePopup |
| 241 | + main.ClosePopup = function() end |
| 242 | + |
| 243 | + local qg = makeQueryGen() |
| 244 | + qg.requesterCallback = function(_, payload) queryJson = payload end |
| 245 | + qg.modWeights = { { tradeModId = "explicit.stat_1", weight = 5, meanStatDiff = 50 } } |
| 246 | + qg.calcContext = baseCalcContext({ "Corrupted Blood cannot be inflicted on you" }) |
| 247 | + qg.itemsTab.items[1] = equippedItem() |
| 248 | + |
| 249 | + qg:FinishQuery() |
| 250 | + main.ClosePopup = originalClosePopup |
| 251 | + |
| 252 | + local query = dkjson.decode(queryJson) |
| 253 | + local andGroup = nil |
| 254 | + for _, g in ipairs(query.query.stats) do |
| 255 | + if g.type == "and" then andGroup = g break end |
| 256 | + end |
| 257 | + assert.is_not_nil(andGroup, "Expected an and-group for the required mod") |
| 258 | + assert.are.equal(CORRUPTED_BLOOD_ID, andGroup.filters[1].id) |
| 259 | + end) |
| 260 | + |
| 261 | + -- Pass: All pinned mods appear in the and-group. |
| 262 | + -- Fail: Only the first mod appears, losing subsequent constraints. |
| 263 | + it("adds multiple required mods to the and-group", function() |
| 264 | + local queryJson |
| 265 | + local originalClosePopup = main.ClosePopup |
| 266 | + main.ClosePopup = function() end |
| 267 | + |
| 268 | + local qg = makeQueryGen() |
| 269 | + qg.requesterCallback = function(_, payload) queryJson = payload end |
| 270 | + qg.modWeights = { { tradeModId = "explicit.stat_1", weight = 10, meanStatDiff = 100 } } |
| 271 | + qg.calcContext = baseCalcContext({ |
| 272 | + "Corrupted Blood cannot be inflicted on you", |
| 273 | + "+42 to maximum Life", |
| 274 | + }) |
| 275 | + qg.itemsTab.items[1] = equippedItem() |
| 276 | + |
| 277 | + qg:FinishQuery() |
| 278 | + main.ClosePopup = originalClosePopup |
| 279 | + |
| 280 | + local query = dkjson.decode(queryJson) |
| 281 | + local andGroup = nil |
| 282 | + for _, g in ipairs(query.query.stats) do |
| 283 | + if g.type == "and" then andGroup = g break end |
| 284 | + end |
| 285 | + assert.is_not_nil(andGroup) |
| 286 | + assert.are.equal(2, #andGroup.filters) |
| 287 | + assert.are.equal(CORRUPTED_BLOOD_ID, andGroup.filters[1].id) |
| 288 | + assert.are.equal(MAX_LIFE_ID, andGroup.filters[2].id) |
| 289 | + end) |
| 290 | + |
| 291 | + -- Pass: Unmappable mod is silently skipped; query still returns successfully. |
| 292 | + -- Fail: FinishQuery crashes or propagates an error, breaking the entire search. |
| 293 | + it("skips unmappable required mods without crashing or corrupting the query", function() |
| 294 | + local queryJson |
| 295 | + local errMsg |
| 296 | + local originalClosePopup = main.ClosePopup |
| 297 | + main.ClosePopup = function() end |
| 298 | + |
| 299 | + local qg = makeQueryGen() |
| 300 | + qg.requesterCallback = function(_, payload, err) |
| 301 | + queryJson = payload |
| 302 | + errMsg = err |
| 303 | + end |
| 304 | + qg.modWeights = { { tradeModId = "explicit.stat_1", weight = 10, meanStatDiff = 100 } } |
| 305 | + qg.calcContext = baseCalcContext({ "This mod cannot be mapped to any trade stat" }) |
| 306 | + qg.itemsTab.items[1] = equippedItem() |
| 307 | + |
| 308 | + local ok = pcall(function() qg:FinishQuery() end) |
| 309 | + main.ClosePopup = originalClosePopup |
| 310 | + |
| 311 | + assert.is_true(ok, "FinishQuery must not crash when a required mod has no trade mapping") |
| 312 | + assert.is_nil(errMsg, "Query should still succeed with the remaining weighted mods") |
| 313 | + local query = dkjson.decode(queryJson) |
| 314 | + -- No and-group since the only pinned mod was unmappable |
| 315 | + assert.are.equal(1, #query.query.stats) |
| 316 | + end) |
| 317 | + end) |
| 318 | + end) |
60 | 319 | end) |
0 commit comments