diff --git a/project/src/controllers/InsuranceController.ts b/project/src/controllers/InsuranceController.ts index 0b40e7d7..df26a000 100644 --- a/project/src/controllers/InsuranceController.ts +++ b/project/src/controllers/InsuranceController.ts @@ -129,7 +129,7 @@ export class InsuranceController this.adoptOrphanedItems(insured); // Send the mail to the player. - this.sendMail(sessionID, insured, insured.items.length === 0); + this.sendMail(sessionID, insured); // Remove the fully processed insurance package from the profile. this.removeInsurancePackageFromProfile(sessionID, insured.messageContent.systemData); @@ -170,11 +170,20 @@ export class InsuranceController const itemsMap = this.populateItemsMap(insured); const parentAttachmentsMap = this.populateParentAttachmentsMap(insured, itemsMap); - // Process all items that are not attached, attachments. Those are handled separately, by value. - this.processRegularItems(insured, toDelete); + // Check to see if any regular items are present. + const hasRegularItems = Array.from(itemsMap.values()).some(item => !this.itemHelper.isAttachmentAttached(item)); - // Process attached, attachments, by value. - this.processAttachments(parentAttachmentsMap, itemsMap, insured.traderId, toDelete); + // Process all items that are not attached, attachments. Those are handled separately, by value. + if (hasRegularItems) + { + this.processRegularItems(insured, toDelete); + } + + // Process attached, attachments, by value, only if there are any. + if (parentAttachmentsMap.size > 0) + { + this.processAttachments(parentAttachmentsMap, itemsMap, insured.traderId, toDelete); + } // Log the number of items marked for deletion, if any if (toDelete.size) @@ -455,14 +464,13 @@ export class InsuranceController * * @param sessionID The session ID that should receive the insurance message. * @param insurance The context of insurance to use. - * @param noItems Whether or not there are any items to return to the player. * @returns void */ - protected sendMail(sessionID: string, insurance: Insurance, noItems: boolean): void + protected sendMail(sessionID: string, insurance: Insurance): void { // After all of the item filtering that we've done, if there are no items remaining, the insurance has // successfully "failed" to return anything and an appropriate message should be sent to the player. - if (noItems) + if (insurance.items.length === 0) { const insuranceFailedTemplates = this.databaseServer.getTables().traders[insurance.traderId].dialogue.insuranceFailed; insurance.messageContent.templateId = this.randomUtil.getArrayValue(insuranceFailedTemplates); @@ -486,10 +494,16 @@ export class InsuranceController * * @param traderId The ID of the trader who insured the item. * @param insuredItem Optional. The item to roll for. Only used for logging. - * @returns true if the insured item should be removed from inventory, false otherwise. + * @returns true if the insured item should be removed from inventory, false otherwise, or null on error. */ - protected rollForDelete(traderId: string, insuredItem?: Item): boolean + protected rollForDelete(traderId: string, insuredItem?: Item): boolean | null { + const trader = this.traderHelper.getTraderById(traderId); + if (!trader) + { + return null; + } + const maxRoll = 9999; const conversionFactor = 100; @@ -499,7 +513,6 @@ export class InsuranceController // Log the roll with as much detail as possible. const itemName = insuredItem ? ` for "${this.itemHelper.getItemName(insuredItem._tpl)}"` : ""; - const trader = this.traderHelper.getTraderById(traderId); const status = roll ? "Delete" : "Keep"; this.logger.debug(`Rolling deletion${itemName} with ${trader} - Return ${traderReturnChance}% - Roll: ${returnChance} - Status: ${status}`); diff --git a/project/tests/controllers/InsuranceController.test.ts b/project/tests/controllers/InsuranceController.test.ts new file mode 100644 index 00000000..313ac8ab --- /dev/null +++ b/project/tests/controllers/InsuranceController.test.ts @@ -0,0 +1,1057 @@ +import "reflect-metadata"; +import { container } from "tsyringe"; +import { vi, beforeAll, afterEach, describe, expect, it } from "vitest"; + +import { InsuranceController } from "@spt-aki/controllers/InsuranceController"; + +import { MessageType } from "@spt-aki/models/enums/MessageType"; +import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; +import { Item } from "@spt-aki/models/eft/common/tables/IItem"; + +describe("InsuranceController", () => +{ + let insuranceController: any; // Using "any" to access private/protected methods without type errors. + + beforeAll(() => + { + insuranceController = container.resolve("InsuranceController"); + }); + + afterEach(() => + { + vi.restoreAllMocks(); + }); + + describe("processReturn", () => + { + /* + it("should process return for all profiles", () => + { + const session1 = "session1"; + const session2 = "session2"; + const profiles = { + [session1]: {}, + [session2]: {} + }; + const getProfilesSpy = vi.spyOn(insuranceController.saveServer, "getProfiles").mockReturnValue(profiles); + const processReturnByProfileSpy = vi.spyOn(insuranceController, "processReturnByProfile"); + + // Execute the method. + insuranceController.processReturn(); + + // Should make a call to get all of the profiles. + expect(getProfilesSpy).toHaveBeenCalledTimes(1); + + // Should process each returned profile. + expect(processReturnByProfileSpy).toHaveBeenCalledTimes(2); + expect(processReturnByProfileSpy).toHaveBeenCalledWith(session1); + expect(processReturnByProfileSpy).toHaveBeenCalledWith(session2); + }); + */ + + it("should not attempt to process profiles if no profiles exist", () => + { + vi.spyOn(insuranceController.saveServer, "getProfiles").mockReturnValue({}); + const processReturnByProfileSpy = vi.spyOn(insuranceController, "processReturnByProfile"); + + // Execute the method. + insuranceController.processReturn(); + + // Should not process any profiles. + expect(processReturnByProfileSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe("findItemsToDelete", () => + { + it("should handle an empty insured object", () => + { + const insured = { items: [] }; + const result = insuranceController.findItemsToDelete(insured); + expect(result.size).toBe(0); + }); + + it("should handle only regular items", () => + { + const mockProcessRegularItems = vi.fn((insured, toDelete) => + { + toDelete.add("item1"); + toDelete.add("item2"); + }); + const mockProcessAttachments = vi.fn(); + + // Spy and replace the real methods with mocks + const mockIsAttachmentAttached = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockReturnValue(false); + vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); + vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments); + + // Create the insured object with only regular items + const insured = { + traderId: "some-trader-id", + items: [ + { _id: "item1", parentId: null }, + { _id: "item2", parentId: null }, + { _id: "item3", parentId: null } + ] + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the correct methods were called and that the result is correct. + expect(mockIsAttachmentAttached).toHaveBeenCalledTimes(4); // Once to see if any attachments are present, once for each item. + expect(mockProcessRegularItems).toHaveBeenCalledWith(insured, expect.any(Set)); + expect(mockProcessAttachments).not.toHaveBeenCalled(); + expect(result.size).toBe(2); + expect(result).toEqual(new Set(["item1", "item2"])); + }); + + it("should handle only attachments", () => + { + // Mock helper methods to simulate only attachments being present. + const mockPopulateItemsMap = vi.fn().mockReturnValue(new Map([ + ["attach1", { _id: "attach1", parentId: "item1" }], + ["attach2", { _id: "attach2", parentId: "item2" }] + ])); + const mockPopulateParentAttachmentsMap = vi.fn().mockReturnValue(new Map([ + ["item1", [{ _id: "attach1", parentId: "item1" }]], + ["item2", [{ _id: "attach2", parentId: "item2" }]] + ])); + const mockProcessRegularItems = vi.fn(); + const mockProcessAttachments = vi.fn((parentAttachmentsMap, itemsMap, traderId, toDelete) => + { + toDelete.add("attach1"); + toDelete.add("attach2"); + }); + + // Spy and replace the real methods with mocks. + vi.spyOn(insuranceController, "populateItemsMap").mockImplementation(mockPopulateItemsMap); + vi.spyOn(insuranceController, "populateParentAttachmentsMap").mockImplementation(mockPopulateParentAttachmentsMap); + vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); + vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments); + + // Create the insured object with only attachments. + const insured = { + traderId: "some-trader-id", + items: [ + { _id: "attach1", parentId: "item1" }, + { _id: "attach2", parentId: "item2" } + ] + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the correct methods were called and that the result is correct. + expect(mockPopulateItemsMap).toHaveBeenCalledWith(insured); + expect(mockPopulateParentAttachmentsMap).toHaveBeenCalledWith(insured, expect.any(Map)); + expect(mockProcessRegularItems).not.toHaveBeenCalled(); + expect(mockProcessAttachments).toHaveBeenCalledWith(expect.any(Map), expect.any(Map), insured.traderId, expect.any(Set)); + expect(result.size).toBe(2); + expect(result).toEqual(new Set(["attach1", "attach2"])); + }); + + it("should handle a mix of regular items and attachments", () => + { + // Mock helper methods to simulate only attachments being present. + const mockPopulateItemsMap = vi.fn().mockReturnValue(new Map([ + ["itemId1", { _id: "itemId1", parentId: null }], // Parent + ["itemId2", { _id: "itemId2", parentId: "itemId1" }] // Attachment + ])); + const mockPopulateParentAttachmentsMap = vi.fn().mockReturnValue(new Map([ + ["itemId1", [{ _id: "itemId2", parentId: "itemId1" }]] + ])); + const mockIsAttachmentAttached = vi.fn().mockReturnValueOnce(false).mockReturnValueOnce(true); + const mockProcessRegularItems = vi.fn(); + const mockProcessAttachments = vi.fn((parentAttachmentsMap, itemsMap, traderId, toDelete) => + { + toDelete.add("itemId2"); + }); + + // Spy and replace the real methods with mocks. + vi.spyOn(insuranceController, "populateItemsMap").mockImplementation(mockPopulateItemsMap); + vi.spyOn(insuranceController, "populateParentAttachmentsMap").mockImplementation(mockPopulateParentAttachmentsMap); + vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation(mockIsAttachmentAttached); + vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); + vi.spyOn(insuranceController, "processAttachments").mockImplementation(mockProcessAttachments); + + const insured = { + traderId: "some-trader", + items: [ + { _id: "itemId1", parentId: null }, + { _id: "itemId2", parentId: "itemId1" } + ] + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the correct methods were called and that the result is correct. + expect(mockPopulateItemsMap).toHaveBeenCalledWith(insured); + expect(mockPopulateParentAttachmentsMap).toHaveBeenCalledWith(insured, expect.any(Map)); + expect(mockIsAttachmentAttached).toHaveBeenCalledTimes(1); + expect(mockProcessRegularItems).toHaveBeenCalledTimes(1); + expect(mockProcessAttachments).toHaveBeenCalledTimes(1); + expect(result.size).toBe(1); + expect(result).toEqual(new Set(["itemId2"])); + }); + + it("should return an empty set if no items are to be deleted", () => + { + const mockIsAttachmentAttached = vi.fn().mockReturnValue(false); + vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation(mockIsAttachmentAttached); + + const insured = { + traderId: "some-trader", + items: [] // No items + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the result is an empty set. + expect(result.size).toBe(0); + expect(result).toEqual(new Set()); + }); + + it("should return a set of items to be deleted", () => + { + const mockIsAttachmentAttached = vi.fn().mockReturnValue(false); + const mockProcessRegularItems = vi.fn((insured, toDelete) => + { + toDelete.add("itemId1"); + }); + + vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation(mockIsAttachmentAttached); + vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); + + const insured = { + traderId: "some-trader", + items: [ + { _id: "itemId1", parentId: null } + ] + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the result is a set containing the item. + expect(result.size).toBe(1); + expect(result).toEqual(new Set(["itemId1"])); + }); + + it("should log the number of items to be deleted", () => + { + const mockIsAttachmentAttached = vi.fn().mockReturnValue(false); + const mockProcessRegularItems = vi.fn((insured, toDelete) => + { + toDelete.add("itemId1").add("itemId2").add("itemId3").add("itemId4"); + }); + + vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation(mockIsAttachmentAttached); + vi.spyOn(insuranceController, "processRegularItems").mockImplementation(mockProcessRegularItems); + const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug"); + + const insured = { + traderId: "some-trader", + items: [ + { _id: "itemId1", parentId: null }, + { _id: "itemId2", parentId: null }, + { _id: "itemId3", parentId: null }, + { _id: "itemId4", parentId: null }, + { _id: "itemId5", parentId: null } + ] + }; + + // Execute the method. + const result = insuranceController.findItemsToDelete(insured); + + // Verify that the result is a set containing the item. + expect(result.size).toBe(4); + expect(loggerDebugSpy).toBeCalledWith("Marked 4 items for deletion from insurance."); + }); + }); + + describe("populateParentAttachmentsMap", () => + { + it("should correctly populate main parent to attachments map", () => + { + const insured = { + items: [ + { _id: "gun", parentId: null, _tpl: "gun_tpl" }, + { _id: "scope", parentId: "gun", _tpl: "scope_tpl" }, + { _id: "muzzle", parentId: "gun", _tpl: "muzzle_tpl" } + ] + }; + + const itemsMap = new Map([ + ["gun", { _id: "gun", parentId: null, _tpl: "gun_tpl" }], + ["scope", { _id: "scope", parentId: "gun", _tpl: "scope_tpl" }], + ["muzzle", { _id: "muzzle", parentId: "gun", _tpl: "muzzle_tpl" }] + ]); + + const isAttachmentAttachedSpy = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation((item: Item) => + { + return item.parentId !== null; + }); + + const isRaidModdableSpy = vi.spyOn(insuranceController.itemHelper, "isRaidModdable").mockReturnValue(true); + const getAttachmentMainParentSpy = vi.spyOn(insuranceController.itemHelper, "getAttachmentMainParent").mockImplementation((itemId: string, map: Map) => + { + return map.get("gun"); + }); + + // Execute the method. + const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); + + // Verify that helper methods are called correctly. + expect(isAttachmentAttachedSpy).toHaveBeenCalledTimes(3); + expect(isRaidModdableSpy).toHaveBeenCalledTimes(2); + expect(getAttachmentMainParentSpy).toHaveBeenCalledTimes(2); + + // Verify that the map is populated correctly. + expect(result.size).toBe(1); + expect(result.get("gun").length).toBe(2); + expect(result.get("gun")[0]._id).toBe("scope"); + expect(result.get("gun")[1]._id).toBe("muzzle"); + }); + + it("should not map items that don't have a parent", () => + { + const insured = { + items: [ + { _id: "item1", parentId: null }, + { _id: "item2", parentId: null } + ] + }; + const itemsMap = new Map(); + itemsMap.set("item1", insured.items[0]); + itemsMap.set("item2", insured.items[1]); + + // Execute the method. + const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); + + // Verify that no items are mapped. + expect(result.size).toBe(0); + }); + + it("should ignore non-raid-moddable items", () => + { + const insured = { + items: [ + { _id: "item1", parentId: "parent1" }, + { _id: "item2", parentId: "parent1" } + ] + }; + const itemsMap = new Map(); + itemsMap.set("item1", insured.items[0]); + itemsMap.set("item2", insured.items[1]); + + // Mock isRaidModdable to return false + vi.spyOn(insuranceController.itemHelper, "isRaidModdable").mockReturnValue(false); + + // Execute the method. + const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); + + // Verify that no items are mapped. + expect(result.size).toBe(0); + }); + + it("should skip attachments where main parent can't be found", () => + { + const insured = { + items: [ + { _id: "item1", parentId: "parent1" }, + { _id: "item2", parentId: "parent1" } + ] + }; + const itemsMap = new Map(); + itemsMap.set("item1", insured.items[0]); + itemsMap.set("item2", insured.items[1]); + + // Mock getAttachmentMainParent to return null. + vi.spyOn(insuranceController.itemHelper, "getAttachmentMainParent").mockReturnValue(null); + + // Execute the method. + const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); + + // Verify that no items are mapped. + expect(result.size).toBe(0); + }); + + it("should correctly handle multiple main parents", () => + { + const insured = { + items: [ + { _id: "item1", parentId: "parent1" }, + { _id: "item2", parentId: "parent1" }, + { _id: "item3", parentId: "parent2" }, + { _id: "item4", parentId: "parent2" } + ] + }; + const itemsMap = new Map(); + itemsMap.set("item1", insured.items[0]); + itemsMap.set("item2", insured.items[1]); + itemsMap.set("item3", insured.items[2]); + itemsMap.set("item4", insured.items[3]); + + // Mock to make all items raid moddable. + vi.spyOn(insuranceController.itemHelper, "isRaidModdable").mockReturnValue(true); + + // Mock to make all items attached. + vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockReturnValue(true); + + // Mock getAttachmentMainParent to return a corresponding parent for each item. + vi.spyOn(insuranceController.itemHelper, "getAttachmentMainParent").mockImplementation((itemId) => + { + return itemsMap.get(itemId).parentId === "parent1" ? { _id: "parent1" } : { _id: "parent2" }; + }); + + // Execute the method. + const result = insuranceController.populateParentAttachmentsMap(insured, itemsMap); + + // Verify that both main parents ("parent1" and "parent2") are mapped correctly. + expect(result.size).toBe(2); + expect(result.get("parent1")).toEqual([{ _id: "item1", parentId: "parent1" }, { _id: "item2", parentId: "parent1" }]); + expect(result.get("parent2")).toEqual([{ _id: "item3", parentId: "parent2" }, { _id: "item4", parentId: "parent2" }]); + }); + }); + + describe("processRegularItems", () => + { + it("should correctly process regular items and their children", () => + { + const insured: { traderId: string, items: unknown } = { + traderId: "some-trader-id", + items: [ + { _id: "item1", parentId: null }, + { _id: "item2", parentId: "item1" }, + { _id: "item3", parentId: null }, + { _id: "item4", parentId: "item3" } + ] as Item[] + }; + const toDelete = new Set(); + + // Mock helper methods and rollForDelete. + const isAttachmentAttachedSpy = vi.spyOn(insuranceController.itemHelper, "isAttachmentAttached").mockImplementation((item: Item) => + { + return item.parentId !== null; + }); + const findAndReturnChildrenAsItemsSpy = vi.spyOn(insuranceController.itemHelper, "findAndReturnChildrenAsItems").mockImplementation((items: Item[], parentId: string) => + { + return items.filter(item => item.parentId === parentId); + }); + const rollForDeleteSpy = vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(true); + + // Execute the method. + insuranceController.processRegularItems(insured, toDelete); + + // Verify behavior. + expect(isAttachmentAttachedSpy).toHaveBeenCalledTimes(6); // Once for each item, one more for each item with a null parentId. + expect(findAndReturnChildrenAsItemsSpy).toHaveBeenCalledTimes(2); // Called only for item1 and item3 + expect(rollForDeleteSpy).toHaveBeenCalledTimes(2); // Called only for item1 and item3 + expect(toDelete).toEqual(new Set(["item1", "item2", "item3", "item4"])); // All items should be marked for deletion + }); + }); + + describe("processAttachments", () => + { + it("should process each set of attachments by their parent items and log parent names", () => + { + const attachments1 = [ + { _id: "attach1", _tpl: "tpl1" }, + { _id: "attach2", _tpl: "tpl2" } + ]; + const attachments2 = [ + { _id: "attach3", _tpl: "tpl3" }, + { _id: "attach4", _tpl: "tpl4" } + ]; + const parent1 = { _id: "parent1", _tpl: "parentTpl1" }; + const parent2 = { _id: "parent2", _tpl: "parentTpl2" }; + const traderId = "some-trader-id"; + const toDelete = new Set(); + + // Mock helper methods. + const processAttachmentByParentSpy = vi.spyOn(insuranceController, "processAttachmentByParent").mockImplementation(() => + {}); + const itemHelperGetItemNameSpy = vi.spyOn(insuranceController.itemHelper, "getItemName").mockImplementation((_tpl) => _tpl); + + // Create maps. + const mainParentToAttachmentsMap = new Map(); + mainParentToAttachmentsMap.set(parent1._id, attachments1); + mainParentToAttachmentsMap.set(parent2._id, attachments2); + + const itemsMap = new Map(); + itemsMap.set(parent1._id, parent1); + itemsMap.set(parent2._id, parent2); + + // Execute the method. + insuranceController.processAttachments(mainParentToAttachmentsMap, itemsMap, traderId, toDelete); + + // Verify that helper methods are called correctly. + expect(processAttachmentByParentSpy).toHaveBeenCalledTimes(2); + expect(processAttachmentByParentSpy).toHaveBeenCalledWith(attachments1, traderId, toDelete); + expect(processAttachmentByParentSpy).toHaveBeenCalledWith(attachments2, traderId, toDelete); + + expect(itemHelperGetItemNameSpy).toHaveBeenCalledTimes(2); + expect(itemHelperGetItemNameSpy).toHaveBeenCalledWith(parent1._tpl); + expect(itemHelperGetItemNameSpy).toHaveBeenCalledWith(parent2._tpl); + }); + }); + + describe("processAttachmentByParent", () => + { + it("should process attachments by calling helper methods in sequence", () => + { + const attachments = [ + { _id: "attach1", _tpl: "tpl1" }, + { _id: "attach2", _tpl: "tpl2" } + ]; + const traderId = "some-trader-id"; + const toDelete = new Set(); + + // Mock helper methods. + const sortAttachmentsByPriceSpy = vi.spyOn(insuranceController, "sortAttachmentsByPrice").mockReturnValue(attachments); + const logAttachmentsDetailsSpy = vi.spyOn(insuranceController, "logAttachmentsDetails").mockImplementation(() => + {}); + const countSuccessfulRollsSpy = vi.spyOn(insuranceController, "countSuccessfulRolls").mockReturnValue(4); + const attachmentDeletionByValueSpy = vi.spyOn(insuranceController, "attachmentDeletionByValue").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.processAttachmentByParent(attachments, traderId, toDelete); + + // Verify that helper methods are called in the correct sequence. + expect(sortAttachmentsByPriceSpy).toHaveBeenCalledWith(attachments); + expect(logAttachmentsDetailsSpy).toHaveBeenCalledWith(attachments); + expect(countSuccessfulRollsSpy).toHaveBeenCalledWith(attachments, traderId); + expect(attachmentDeletionByValueSpy).toHaveBeenCalledWith(attachments, 4, toDelete); + }); + }); + + describe("sortAttachmentsByPrice", () => + { + it("should sort the attachments array by maxPrice in descending order", () => + { + const attachments = [ + { _id: "item1", _tpl: "tpl1" }, + { _id: "item2", _tpl: "tpl2" }, + { _id: "item3", _tpl: "tpl3" } + ]; + + const itemHelper = { + getItemName: vi.fn((tpl) => `Item Name ${tpl}`), + getItemMaxPrice: vi.fn((tpl) => + { + if (tpl === "tpl1") return 100; + if (tpl === "tpl2") return 200; + if (tpl === "tpl3") return 50; + return 0; + }) + }; + + // Mock the itemHelper methods. + vi.spyOn(insuranceController.itemHelper, "getItemName").mockImplementation(itemHelper.getItemName); + vi.spyOn(insuranceController.itemHelper, "getItemMaxPrice").mockImplementation(itemHelper.getItemMaxPrice); + + // Execute the method. + const result = insuranceController.sortAttachmentsByPrice(attachments); + + // Verify that the array is sorted by maxPrice in descending order. + expect(result[0].maxPrice).toBe(200); + expect(result[1].maxPrice).toBe(100); + expect(result[2].maxPrice).toBe(50); + }); + + it("should handle null max-price values by sorting them to the bottom", () => + { + const attachments = [ + { _id: "item1", _tpl: "tpl1" }, + { _id: "item2", _tpl: "tpl2" }, + { _id: "item3", _tpl: "tpl3" } + ]; + + const itemHelper = { + getItemName: vi.fn((tpl) => `Item Name ${tpl}`), + getItemMaxPrice: vi.fn((tpl) => + { + if (tpl === "tpl1") return null; + if (tpl === "tpl2") return 200; + if (tpl === "tpl3") return 50; + return 0; + }) + }; + + // Mock the itemHelper methods. + vi.spyOn(insuranceController.itemHelper, "getItemName").mockImplementation(itemHelper.getItemName); + vi.spyOn(insuranceController.itemHelper, "getItemMaxPrice").mockImplementation(itemHelper.getItemMaxPrice); + + // Execute the method. + const result = insuranceController.sortAttachmentsByPrice(attachments); + + // Verify that the array is sorted by maxPrice in descending order. + expect(result[0].maxPrice).toBe(200); + expect(result[1].maxPrice).toBe(50); + expect(result[2].maxPrice).toBe(null); + }); + }); + + describe("logAttachmentsDetails", () => + { + it("should log details for each attachment", () => + { + const attachments = [ + { _id: "item1", name: "Item 1", maxPrice: 100 }, + { _id: "item2", name: "Item 2", maxPrice: 200 } + ]; + + // Mock the logger.debug function. + const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.logAttachmentsDetails(attachments); + + // Verify that logger.debug was called correctly. + expect(loggerDebugSpy).toHaveBeenCalledTimes(2); + expect(loggerDebugSpy).toHaveBeenNthCalledWith(1, "Child Item - Name: Item 1, Max Price: 100"); + expect(loggerDebugSpy).toHaveBeenNthCalledWith(2, "Child Item - Name: Item 2, Max Price: 200"); + }); + }); + + describe("countSuccessfulRolls", () => + { + it("should count the number of successful rolls based on the rollForDelete method", () => + { + const attachments = [ + { _id: "attach1", name: "Attachment 1" }, + { _id: "attach2", name: "Attachment 2" }, + { _id: "attach3", name: "Attachment 3" } + ]; + const traderId = "some-trader-id"; + + // Create a deterministic sequence of "random" values for the test. + const randomSequence = [0.6, 0.4, 0.6]; // Two rolls > 0.5 and one roll < 0.5 + let i = 0; + const originalRandom = Math.random; + Math.random = vi.fn(() => randomSequence[i++]); + + // Mock rollForDelete to return based on our "random" values. + vi.spyOn(insuranceController, "rollForDelete").mockImplementation((id) => + { + return id === traderId && Math.random() > 0.5; + }); + + // Execute the method. + const result = insuranceController.countSuccessfulRolls(attachments, traderId); + + // Verify that two successful rolls were counted (first and third items). + expect(result).toBe(2); + + // Restore the original Math.random function. + Math.random = originalRandom; + }); + + it("should return zero if there are no successful rolls", () => + { + const attachments = [ + { _id: "attach1", name: "Attachment 1" } + ]; + const traderId = "some-trader-id"; + + // Mock rollForDelete to always return false. + vi.spyOn(insuranceController, "rollForDelete").mockReturnValue(false); + + // Execute the method. + const result = insuranceController.countSuccessfulRolls(attachments, traderId); + + // Verify that zero successful rolls were returned. + expect(result).toBe(0); + }); + + it("should return zero if there are no attachments", () => + { + const attachments = []; + const traderId = "some-trader-id"; + + // Execute the method. + const result = insuranceController.countSuccessfulRolls(attachments, traderId); + + // Verify that zero successful rolls were returned. + expect(result).toBe(0); + }); + }); + + describe("attachmentDeletionByValue", () => + { + it("should add attachments to the toDelete set based on successfulRolls", () => + { + const attachments = [ + { _id: "attach1", name: "Attachment 1", maxPrice: 300 }, + { _id: "attach2", name: "Attachment 2", maxPrice: 200 }, + { _id: "attach3", name: "Attachment 3", maxPrice: 100 } + ]; + const successfulRolls = 2; + const toDelete = new Set(); + + const loggerDebugSpy = vi.spyOn(insuranceController.logger, "debug").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); + + // Should add the first two valuable attachments to the toDelete set. + expect(toDelete).toEqual(new Set(["attach1", "attach2"])); + + // Verify that logger.debug was called twice. + expect(loggerDebugSpy).toHaveBeenCalledTimes(2); + }); + + it("should not add any attachments to toDelete if successfulRolls is zero", () => + { + const attachments = [ + { _id: "attach1", name: "Attachment 1", maxPrice: 100 } + ]; + const successfulRolls = 0; + const toDelete = new Set(); + + // Execute the method. + insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); + + // Verify that no attachments are added to the toDelete set. + expect(toDelete).toEqual(new Set([])); + }); + + it("should add all attachments to toDelete if successfulRolls is greater than the number of attachments", () => + { + const attachments = [ + { _id: "attach1", name: "Attachment 1", maxPrice: 100 }, + { _id: "attach2", name: "Attachment 2", maxPrice: 200 } + ]; + const successfulRolls = 3; + const toDelete = new Set(); + + // Execute the method. + insuranceController.attachmentDeletionByValue(attachments, successfulRolls, toDelete); + + // Verify that all attachments are added to the toDelete set. + expect(toDelete).toEqual(new Set(["attach1", "attach2"])); + }); + }); + + describe("removeItemsFromInsurance", () => + { + it("should remove items from insurance based on the toDelete set", () => + { + const insured = { + items: [ + { _id: "item1" }, + { _id: "item2" }, + { _id: "item3" } + ] + }; + const toDelete = new Set(["item1", "item3"]); + + // Execute the method. + insuranceController.removeItemsFromInsurance(insured, toDelete); + + // Verify that items with _id "item1" and "item3" are removed + expect(insured.items).toEqual([{ _id: "item2" }]); + }); + + it("should not remove any items if toDelete set is empty", () => + { + const insured = { + items: [ + { _id: "item1" }, + { _id: "item2" }, + { _id: "item3" } + ] + }; + const toDelete = new Set(); + + // Execute the method. + insuranceController.removeItemsFromInsurance(insured, toDelete); + + // Verify that no items are removed. + expect(insured.items).toEqual([ + { _id: "item1" }, + { _id: "item2" }, + { _id: "item3" } + ]); + }); + + it("should leave the insurance items empty if all are to be deleted", () => + { + const insured = { + items: [ + { _id: "item1" }, + { _id: "item2" } + ] + }; + const toDelete = new Set(["item1", "item2"]); + + // Execute the method. + insuranceController.removeItemsFromInsurance(insured, toDelete); + + // Verify that all items are removed. + expect(insured.items).toEqual([]); + }); + }); + + describe("adoptOrphanedItems", () => + { + it("should adopt orphaned items by resetting them as base-level items", () => + { + const insured = { + items: [ + { _id: "1", parentId: "999", slotId: "main" }, // This is orphaned. + { _id: "2", parentId: "1", slotId: "main" } + ] + }; + const hideoutParentId = "hideout-parent"; + + vi.spyOn(insuranceController, "fetchHideoutItemParent").mockReturnValue(hideoutParentId); + + // Execute the method. + insuranceController.adoptOrphanedItems(insured); + + // Verify that the item with _id "1" has been adopted. + expect(insured.items[0].parentId).toBe(hideoutParentId); + expect(insured.items[0].slotId).toBe("hideout"); + }); + + it("should not adopt items that are not orphaned", () => + { + const insured = { + items: [ + { _id: "1", parentId: "999", slotId: "main" }, + { _id: "2", parentId: "1", slotId: "main" } // This is not orphaned. + ] + }; + const hideoutParentId = "hideout-parent"; + + vi.spyOn(insuranceController, "fetchHideoutItemParent").mockReturnValue(hideoutParentId); + + // Execute the method. + insuranceController.adoptOrphanedItems(insured); + + // Verify that the item with _id "2" has not been adopted. + expect(insured.items[1].parentId).toBe("1"); + expect(insured.items[1].slotId).not.toBe("hideout"); + }); + + it("should remove location data from adopted items", () => + { + const insured = { + items: [ + { _id: "1", parentId: "999", slotId: "main", location: "location-value" }, // This is orphaned. + { _id: "2", parentId: "1", slotId: "main", location: "location-value" } + ] + }; + const hideoutParentId = "hideout-parent"; + + vi.spyOn(insuranceController, "fetchHideoutItemParent").mockReturnValue(hideoutParentId); + + // Execute the method. + insuranceController.adoptOrphanedItems(insured); + + // Verify that the item with _id "1" has no location data. + expect(insured.items[0]).not.toHaveProperty("location", "location-value"); + }); + }); + + describe("fetchHideoutItemParent", () => + { + it("should return the parentId of the hideout item if it exists", () => + { + const hideoutId = "hideout_id"; + const items = [ + { id: "1", slotId: "hideout", parentId: hideoutId }, + { id: "2", slotId: "main", parentId: "not_hideout_id" } + ]; + + // Execute the method. + const result = insuranceController.fetchHideoutItemParent(items); + + // Verify that the hideout item parentId is returned. + expect(result).toBe(hideoutId); + }); + + it("should return an empty string if the hideout item does not exist", () => + { + const items = [ + { id: "1", slotId: "mod_suppressor", parentId: "not_hideout_id" }, + { id: "2", slotId: "main", parentId: "not_hideout_id" } + ]; + + // Execute the method. + const result = insuranceController.fetchHideoutItemParent(items); + + // Verify that an empty string is returned. + expect(result).toBe(""); + }); + }); + + describe("sendMail", () => + { + it("should send insurance failed message when no items are present", () => + { + const traderHelper = container.resolve("TraderHelper"); + + const sessionID = "someSessionId"; + const insuranceFailedTpl = "failed-message-template"; + const insurance = { + traderId: "54cb57776803fa99248b456e", // Therapist + messageContent: { + templateId: null, + maxStorageTime: 100, + systemData: {} + }, + items: [] + }; + + // Mock the randomUtil to return a static failed template string. + vi.spyOn(insuranceController.randomUtil, "getArrayValue").mockReturnValue(insuranceFailedTpl); + + // Don't actually send the message. + const sendLocalisedNpcMessageToPlayerSpy = vi.spyOn(insuranceController.mailSendService, "sendLocalisedNpcMessageToPlayer").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.sendMail(sessionID, insurance); + + // Verify that the insurance failed message was sent. + expect(sendLocalisedNpcMessageToPlayerSpy).toHaveBeenCalledWith( + sessionID, + traderHelper.getTraderById(insurance.traderId), + MessageType.INSURANCE_RETURN, + insuranceFailedTpl, + insurance.items, + insurance.messageContent.maxStorageTime, + insurance.messageContent.systemData + ); + }); + + it("should not send insurance failed message when items are present", () => + { + const traderHelper = container.resolve("TraderHelper"); + + const sessionID = "someSessionId"; + const itemMessageTpl = "item-message-template"; + const insuranceFailedTpl = "failed-message-template"; + const insurance = { + traderId: "54cb57776803fa99248b456e", // Therapist + messageContent: { + templateId: itemMessageTpl, + maxStorageTime: 100, + systemData: {} + }, + items: ["item1", "item2"] + }; + + // Mock the randomUtil to return a static failed template string. + vi.spyOn(insuranceController.randomUtil, "getArrayValue").mockReturnValue(insuranceFailedTpl); + + // Don't actually send the message. + const sendLocalisedNpcMessageToPlayerSpy = vi.spyOn(insuranceController.mailSendService, "sendLocalisedNpcMessageToPlayer").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.sendMail(sessionID, insurance); + + // Verify that the insurance failed message was not sent. + expect(sendLocalisedNpcMessageToPlayerSpy).toHaveBeenCalledWith( + sessionID, + traderHelper.getTraderById(insurance.traderId), + MessageType.INSURANCE_RETURN, + itemMessageTpl, + insurance.items, + insurance.messageContent.maxStorageTime, + insurance.messageContent.systemData + ); + }); + }); + + describe("rollForDelete", () => + { + it("should return true when random roll is equal to trader return chance", () => + { + vi.spyOn(insuranceController.randomUtil, "getInt").mockReturnValue(8500); // Our "random" roll. + const traderId = "54cb57776803fa99248b456e"; // Therapist (85% return chance) + insuranceController.insuranceConfig = { + returnChancePercent: { + [traderId]: 85 // Force 85% return chance + } + }; + + // Execute the method. + const result = insuranceController.rollForDelete(traderId); + + // Verify that the result is true. + expect(result).toBe(true); + }); + + it("should return true when random roll is greater than trader return chance", () => + { + vi.spyOn(insuranceController.randomUtil, "getInt").mockReturnValue(8501); // Our "random" roll. + const traderId = "54cb57776803fa99248b456e"; // Therapist (85% return chance) + insuranceController.insuranceConfig = { + returnChancePercent: { + [traderId]: 85 // Force 85% return chance + } + }; + + // Execute the method. + const result = insuranceController.rollForDelete(traderId); + + // Verify that the result is true. + expect(result).toBe(true); + }); + + it("should return false when random roll is less than trader return chance", () => + { + vi.spyOn(insuranceController.randomUtil, "getInt").mockReturnValue(8499); // Our "random" roll. + const traderId = "54cb57776803fa99248b456e"; // Therapist (85% return chance) + insuranceController.insuranceConfig = { + returnChancePercent: { + [traderId]: 85 // Force 85% return chance + } + }; + + // Execute the method. + const result = insuranceController.rollForDelete(traderId); + + // Verify that the result is false. + expect(result).toBe(false); + }); + + it("should log error if trader can not be found", () => + { + const traderId = "invalid-trader-id"; + + const loggerErrorSpy = vi.spyOn(insuranceController.logger, "error").mockImplementation(() => + {}); + + // Execute the method. + insuranceController.rollForDelete(traderId); + + // Verify that the logger.error method was called. + expect(loggerErrorSpy).toHaveBeenCalled(); + }); + + it("should return null if trader can not be found", () => + { + const traderId = "invalid-trader-id"; + + vi.spyOn(insuranceController.logger, "error").mockImplementation(() => + {}); + + // Execute the method. + const result = insuranceController.rollForDelete(traderId); + + // Verify that the result is null. + expect(result).toBe(null); + }); + }); +});