diff --git a/project/package.json b/project/package.json index fbacf47d..d85f78dc 100644 --- a/project/package.json +++ b/project/package.json @@ -14,7 +14,7 @@ "check:circular": "madge --circular --extensions ts ./src/", "lint": "rome ci src --formatter-enabled=false --max-diagnostics=200", "lint:fix": "eslint --fix --ext .ts src/**", - "test:run": "jest --colors --runInBand", + "test": "jest --colors --runInBand", "test:coverage": "jest --coverage --maxWorkers=1 --no-cache", "build:release": "cross-env PKG_CACHE_PATH=\"./.pkg-cache\" gulp build:release", "build:debug": "cross-env PKG_CACHE_PATH=\"./.pkg-cache\" gulp build:debug", diff --git a/project/src/di/Container.ts b/project/src/di/Container.ts index a0fc804a..be6f5bcc 100644 --- a/project/src/di/Container.ts +++ b/project/src/di/Container.ts @@ -250,16 +250,16 @@ import { WinstonRequestLogger } from "@spt-aki/utils/logging/WinstonRequestLogge /** * Handle the registration of classes to be used by the Dependency Injection code */ -export class Container +export class Container { - - public static registerPostLoadTypes(container: DependencyContainer, childContainer: DependencyContainer):void + + public static registerPostLoadTypes(container: DependencyContainer, childContainer: DependencyContainer):void { container.register("AkiHttpListener", AkiHttpListener, {lifecycle: Lifecycle.Singleton}); childContainer.registerType("HttpListener", "AkiHttpListener"); } - public static registerTypes(depContainer: DependencyContainer): void + public static registerTypes(depContainer: DependencyContainer): void { depContainer.register("ApplicationContext", ApplicationContext, { lifecycle: Lifecycle.Singleton }); Container.registerUtils(depContainer); @@ -275,13 +275,13 @@ export class Container Container.registerCallbacks(depContainer); Container.registerServers(depContainer); - + Container.registerServices(depContainer); Container.registerControllers(depContainer); } - public static registerListTypes(depContainer: DependencyContainer): void + public static registerListTypes(depContainer: DependencyContainer): void { depContainer.register("OnLoadModService", { useValue: new OnLoadModService(depContainer) }); depContainer.register("HttpListenerModService", { useValue: new HttpListenerModService(depContainer) }); @@ -360,7 +360,7 @@ export class Container depContainer.registerType("SaveLoadRouter", "ProfileSaveLoadRouter"); } - private static registerUtils(depContainer: DependencyContainer): void + private static registerUtils(depContainer: DependencyContainer): void { // Utils depContainer.register("App", App, { lifecycle: Lifecycle.Singleton }); @@ -386,7 +386,7 @@ export class Container depContainer.register("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton }); } - private static registerRouters(depContainer: DependencyContainer): void + private static registerRouters(depContainer: DependencyContainer): void { // Routers depContainer.register("HttpRouter", HttpRouter, { lifecycle: Lifecycle.Singleton }); @@ -454,7 +454,7 @@ export class Container depContainer.register("WeatherStaticRouter", { useClass: WeatherStaticRouter }); } - private static registerGenerators(depContainer: DependencyContainer): void + private static registerGenerators(depContainer: DependencyContainer): void { // Generators depContainer.register("BotGenerator", BotGenerator); @@ -473,8 +473,8 @@ export class Container depContainer.register("BotLevelGenerator", { useClass: BotLevelGenerator }); depContainer.register("BotEquipmentModGenerator", { useClass: BotEquipmentModGenerator }); depContainer.register("RepeatableQuestGenerator", { useClass: RepeatableQuestGenerator }); - - + + depContainer.register("BarrelInventoryMagGen", { useClass: BarrelInventoryMagGen }); depContainer.register("ExternalInventoryMagGen", { useClass: ExternalInventoryMagGen }); depContainer.register("InternalMagazineInventoryMagGen", { useClass: InternalMagazineInventoryMagGen }); @@ -488,7 +488,7 @@ export class Container } - private static registerHelpers(depContainer: DependencyContainer): void + private static registerHelpers(depContainer: DependencyContainer): void { // Helpers depContainer.register("AssortHelper", { useClass: AssortHelper }); @@ -530,7 +530,7 @@ export class Container depContainer.register("RepeatableQuestHelper", { useClass: RepeatableQuestHelper }); } - private static registerLoaders(depContainer: DependencyContainer): void + private static registerLoaders(depContainer: DependencyContainer): void { // Loaders depContainer.register("BundleLoader", BundleLoader, { lifecycle: Lifecycle.Singleton }); @@ -538,7 +538,7 @@ export class Container depContainer.register("PostAkiModLoader", PostAkiModLoader, { lifecycle: Lifecycle.Singleton }); } - private static registerCallbacks(depContainer: DependencyContainer): void + private static registerCallbacks(depContainer: DependencyContainer): void { // Callbacks depContainer.register("BotCallbacks", { useClass: BotCallbacks }); @@ -576,7 +576,7 @@ export class Container depContainer.register("WishlistCallbacks", { useClass: WishlistCallbacks }); } - private static registerServices(depContainer: DependencyContainer): void + private static registerServices(depContainer: DependencyContainer): void { // Services depContainer.register("ImageRouteService", ImageRouteService, { lifecycle: Lifecycle.Singleton }); @@ -621,7 +621,7 @@ export class Container depContainer.register("MailSendService", MailSendService); } - private static registerServers(depContainer: DependencyContainer): void + private static registerServers(depContainer: DependencyContainer): void { // Servers depContainer.register("DatabaseServer", DatabaseServer, { lifecycle: Lifecycle.Singleton }); @@ -634,7 +634,7 @@ export class Container } - private static registerControllers(depContainer: DependencyContainer): void + private static registerControllers(depContainer: DependencyContainer): void { // Controllers depContainer.register("BotController", { useClass: BotController }); @@ -665,4 +665,4 @@ export class Container depContainer.register("WeatherController", { useClass: WeatherController }); depContainer.register("WishlistController", WishlistController); } -} \ No newline at end of file +} diff --git a/project/tests/CustomEnvironment.ts b/project/tests/CustomEnvironment.ts index c7420158..424177d7 100644 --- a/project/tests/CustomEnvironment.ts +++ b/project/tests/CustomEnvironment.ts @@ -1,33 +1,707 @@ import "reflect-metadata"; -import { container } from "tsyringe"; +import { container, DependencyContainer, Lifecycle } from "tsyringe"; +// Import everything we intend to test so we can register it in the Jest custom environment. +import { BotCallbacks } from "@spt-aki/callbacks/BotCallbacks"; +import { BundleCallbacks } from "@spt-aki/callbacks/BundleCallbacks"; +import { ClientLogCallbacks } from "@spt-aki/callbacks/ClientLogCallbacks"; +import { CustomizationCallbacks } from "@spt-aki/callbacks/CustomizationCallbacks"; +import { DataCallbacks } from "@spt-aki/callbacks/DataCallbacks"; +import { DialogueCallbacks } from "@spt-aki/callbacks/DialogueCallbacks"; +import { GameCallbacks } from "@spt-aki/callbacks/GameCallbacks"; +import { HandbookCallbacks } from "@spt-aki/callbacks/HandbookCallbacks"; +import { HealthCallbacks } from "@spt-aki/callbacks/HealthCallbacks"; +import { HideoutCallbacks } from "@spt-aki/callbacks/HideoutCallbacks"; +import { HttpCallbacks } from "@spt-aki/callbacks/HttpCallbacks"; +import { InraidCallbacks } from "@spt-aki/callbacks/InraidCallbacks"; +import { InsuranceCallbacks } from "@spt-aki/callbacks/InsuranceCallbacks"; +import { InventoryCallbacks } from "@spt-aki/callbacks/InventoryCallbacks"; +import { ItemEventCallbacks } from "@spt-aki/callbacks/ItemEventCallbacks"; +import { LauncherCallbacks } from "@spt-aki/callbacks/LauncherCallbacks"; +import { LocationCallbacks } from "@spt-aki/callbacks/LocationCallbacks"; +import { MatchCallbacks } from "@spt-aki/callbacks/MatchCallbacks"; +import { ModCallbacks } from "@spt-aki/callbacks/ModCallbacks"; +import { NoteCallbacks } from "@spt-aki/callbacks/NoteCallbacks"; +import { NotifierCallbacks } from "@spt-aki/callbacks/NotifierCallbacks"; +import { PresetBuildCallbacks } from "@spt-aki/callbacks/PresetBuildCallbacks"; +import { PresetCallbacks } from "@spt-aki/callbacks/PresetCallbacks"; +import { ProfileCallbacks } from "@spt-aki/callbacks/ProfileCallbacks"; +import { QuestCallbacks } from "@spt-aki/callbacks/QuestCallbacks"; +import { RagfairCallbacks } from "@spt-aki/callbacks/RagfairCallbacks"; +import { RepairCallbacks } from "@spt-aki/callbacks/RepairCallbacks"; +import { SaveCallbacks } from "@spt-aki/callbacks/SaveCallbacks"; +import { TradeCallbacks } from "@spt-aki/callbacks/TradeCallbacks"; +import { TraderCallbacks } from "@spt-aki/callbacks/TraderCallbacks"; +import { WeatherCallbacks } from "@spt-aki/callbacks/WeatherCallbacks"; +import { WishlistCallbacks } from "@spt-aki/callbacks/WishlistCallbacks"; +import { ApplicationContext } from "@spt-aki/context/ApplicationContext"; +import { BotController } from "@spt-aki/controllers/BotController"; +import { ClientLogController } from "@spt-aki/controllers/ClientLogController"; +import { CustomizationController } from "@spt-aki/controllers/CustomizationController"; +import { DialogueController } from "@spt-aki/controllers/DialogueController"; +import { GameController } from "@spt-aki/controllers/GameController"; +import { HandbookController } from "@spt-aki/controllers/HandbookController"; +import { HealthController } from "@spt-aki/controllers/HealthController"; +import { HideoutController } from "@spt-aki/controllers/HideoutController"; +import { InraidController } from "@spt-aki/controllers/InraidController"; +import { InsuranceController } from "@spt-aki/controllers/InsuranceController"; +import { InventoryController } from "@spt-aki/controllers/InventoryController"; +import { LauncherController } from "@spt-aki/controllers/LauncherController"; +import { LocationController } from "@spt-aki/controllers/LocationController"; +import { MatchController } from "@spt-aki/controllers/MatchController"; +import { NoteController } from "@spt-aki/controllers/NoteController"; +import { NotifierController } from "@spt-aki/controllers/NotifierController"; +import { PresetBuildController } from "@spt-aki/controllers/PresetBuildController"; +import { PresetController } from "@spt-aki/controllers/PresetController"; +import { ProfileController } from "@spt-aki/controllers/ProfileController"; +import { QuestController } from "@spt-aki/controllers/QuestController"; +import { RagfairController } from "@spt-aki/controllers/RagfairController"; +import { RepairController } from "@spt-aki/controllers/RepairController"; +import { RepeatableQuestController } from "@spt-aki/controllers/RepeatableQuestController"; +import { TradeController } from "@spt-aki/controllers/TradeController"; +import { TraderController } from "@spt-aki/controllers/TraderController"; +import { WeatherController } from "@spt-aki/controllers/WeatherController"; +import { WishlistController } from "@spt-aki/controllers/WishlistController"; +import { BotEquipmentModGenerator } from "@spt-aki/generators/BotEquipmentModGenerator"; +import { BotGenerator } from "@spt-aki/generators/BotGenerator"; +import { BotInventoryGenerator } from "@spt-aki/generators/BotInventoryGenerator"; +import { BotLevelGenerator } from "@spt-aki/generators/BotLevelGenerator"; +import { BotLootGenerator } from "@spt-aki/generators/BotLootGenerator"; +import { BotWeaponGenerator } from "@spt-aki/generators/BotWeaponGenerator"; +import { FenceBaseAssortGenerator } from "@spt-aki/generators/FenceBaseAssortGenerator"; +import { LocationGenerator } from "@spt-aki/generators/LocationGenerator"; +import { LootGenerator } from "@spt-aki/generators/LootGenerator"; +import { PMCLootGenerator } from "@spt-aki/generators/PMCLootGenerator"; +import { PlayerScavGenerator } from "@spt-aki/generators/PlayerScavGenerator"; +import { RagfairAssortGenerator } from "@spt-aki/generators/RagfairAssortGenerator"; +import { RagfairOfferGenerator } from "@spt-aki/generators/RagfairOfferGenerator"; +import { RepeatableQuestGenerator } from "@spt-aki/generators/RepeatableQuestGenerator"; +import { ScavCaseRewardGenerator } from "@spt-aki/generators/ScavCaseRewardGenerator"; +import { WeatherGenerator } from "@spt-aki/generators/WeatherGenerator"; +import { BarrelInventoryMagGen } from "@spt-aki/generators/weapongen/implementations/BarrelInventoryMagGen"; +import { ExternalInventoryMagGen } from "@spt-aki/generators/weapongen/implementations/ExternalInventoryMagGen"; +import { InternalMagazineInventoryMagGen } from "@spt-aki/generators/weapongen/implementations/InternalMagazineInventoryMagGen"; +import { UbglExternalMagGen } from "@spt-aki/generators/weapongen/implementations/UbglExternalMagGen"; +import { AssortHelper } from "@spt-aki/helpers/AssortHelper"; +import { BotDifficultyHelper } from "@spt-aki/helpers/BotDifficultyHelper"; +import { BotGeneratorHelper } from "@spt-aki/helpers/BotGeneratorHelper"; +import { BotHelper } from "@spt-aki/helpers/BotHelper"; +import { BotWeaponGeneratorHelper } from "@spt-aki/helpers/BotWeaponGeneratorHelper"; +import { ContainerHelper } from "@spt-aki/helpers/ContainerHelper"; +import { DialogueHelper } from "@spt-aki/helpers/DialogueHelper"; +import { DurabilityLimitsHelper } from "@spt-aki/helpers/DurabilityLimitsHelper"; +import { GameEventHelper } from "@spt-aki/helpers/GameEventHelper"; +import { HandbookHelper } from "@spt-aki/helpers/HandbookHelper"; +import { HealthHelper } from "@spt-aki/helpers/HealthHelper"; +import { HideoutHelper } from "@spt-aki/helpers/HideoutHelper"; +import { HttpServerHelper } from "@spt-aki/helpers/HttpServerHelper"; +import { InRaidHelper } from "@spt-aki/helpers/InRaidHelper"; +import { InventoryHelper } from "@spt-aki/helpers/InventoryHelper"; +import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; +import { NotificationSendHelper } from "@spt-aki/helpers/NotificationSendHelper"; +import { NotifierHelper } from "@spt-aki/helpers/NotifierHelper"; +import { PaymentHelper } from "@spt-aki/helpers/PaymentHelper"; +import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; +import { ProbabilityHelper } from "@spt-aki/helpers/ProbabilityHelper"; +import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper"; +import { QuestConditionHelper } from "@spt-aki/helpers/QuestConditionHelper"; +import { QuestHelper } from "@spt-aki/helpers/QuestHelper"; +import { RagfairHelper } from "@spt-aki/helpers/RagfairHelper"; +import { RagfairOfferHelper } from "@spt-aki/helpers/RagfairOfferHelper"; +import { RagfairSellHelper } from "@spt-aki/helpers/RagfairSellHelper"; +import { RagfairServerHelper } from "@spt-aki/helpers/RagfairServerHelper"; +import { RagfairSortHelper } from "@spt-aki/helpers/RagfairSortHelper"; +import { RepairHelper } from "@spt-aki/helpers/RepairHelper"; +import { RepeatableQuestHelper } from "@spt-aki/helpers/RepeatableQuestHelper"; +import { SecureContainerHelper } from "@spt-aki/helpers/SecureContainerHelper"; +import { TradeHelper } from "@spt-aki/helpers/TradeHelper"; +import { TraderAssortHelper } from "@spt-aki/helpers/TraderAssortHelper"; +import { TraderHelper } from "@spt-aki/helpers/TraderHelper"; +import { UtilityHelper } from "@spt-aki/helpers/UtilityHelper"; +import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; +import { BundleLoader } from "@spt-aki/loaders/BundleLoader"; +import { ModLoadOrder } from "@spt-aki/loaders/ModLoadOrder"; +import { ModTypeCheck } from "@spt-aki/loaders/ModTypeCheck"; +import { PostAkiModLoader } from "@spt-aki/loaders/PostAkiModLoader"; +import { PostDBModLoader } from "@spt-aki/loaders/PostDBModLoader"; +import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader"; +import { IAsyncQueue } from "@spt-aki/models/spt/utils/IAsyncQueue"; +import { IUUidGenerator } from "@spt-aki/models/spt/utils/IUuidGenerator"; +import { EventOutputHolder } from "@spt-aki/routers/EventOutputHolder"; +import { HttpRouter } from "@spt-aki/routers/HttpRouter"; +import { ImageRouter } from "@spt-aki/routers/ImageRouter"; +import { ItemEventRouter } from "@spt-aki/routers/ItemEventRouter"; +import { BotDynamicRouter } from "@spt-aki/routers/dynamic/BotDynamicRouter"; +import { BundleDynamicRouter } from "@spt-aki/routers/dynamic/BundleDynamicRouter"; +import { CustomizationDynamicRouter } from "@spt-aki/routers/dynamic/CustomizationDynamicRouter"; +import { DataDynamicRouter } from "@spt-aki/routers/dynamic/DataDynamicRouter"; +import { HttpDynamicRouter } from "@spt-aki/routers/dynamic/HttpDynamicRouter"; +import { InraidDynamicRouter } from "@spt-aki/routers/dynamic/InraidDynamicRouter"; +import { LocationDynamicRouter } from "@spt-aki/routers/dynamic/LocationDynamicRouter"; +import { NotifierDynamicRouter } from "@spt-aki/routers/dynamic/NotifierDynamicRouter"; +import { TraderDynamicRouter } from "@spt-aki/routers/dynamic/TraderDynamicRouter"; +import { CustomizationItemEventRouter } from "@spt-aki/routers/item_events/CustomizationItemEventRouter"; +import { HealthItemEventRouter } from "@spt-aki/routers/item_events/HealthItemEventRouter"; +import { HideoutItemEventRouter } from "@spt-aki/routers/item_events/HideoutItemEventRouter"; +import { InsuranceItemEventRouter } from "@spt-aki/routers/item_events/InsuranceItemEventRouter"; +import { InventoryItemEventRouter } from "@spt-aki/routers/item_events/InventoryItemEventRouter"; +import { NoteItemEventRouter } from "@spt-aki/routers/item_events/NoteItemEventRouter"; +import { PresetBuildItemEventRouter } from "@spt-aki/routers/item_events/PresetBuildItemEventRouter"; +import { QuestItemEventRouter } from "@spt-aki/routers/item_events/QuestItemEventRouter"; +import { RagfairItemEventRouter } from "@spt-aki/routers/item_events/RagfairItemEventRouter"; +import { RepairItemEventRouter } from "@spt-aki/routers/item_events/RepairItemEventRouter"; +import { TradeItemEventRouter } from "@spt-aki/routers/item_events/TradeItemEventRouter"; +import { WishlistItemEventRouter } from "@spt-aki/routers/item_events/WishlistItemEventRouter"; +import { HealthSaveLoadRouter } from "@spt-aki/routers/save_load/HealthSaveLoadRouter"; +import { InraidSaveLoadRouter } from "@spt-aki/routers/save_load/InraidSaveLoadRouter"; +import { InsuranceSaveLoadRouter } from "@spt-aki/routers/save_load/InsuranceSaveLoadRouter"; +import { ProfileSaveLoadRouter } from "@spt-aki/routers/save_load/ProfileSaveLoadRouter"; +import { BundleSerializer } from "@spt-aki/routers/serializers/BundleSerializer"; +import { ImageSerializer } from "@spt-aki/routers/serializers/ImageSerializer"; +import { NotifySerializer } from "@spt-aki/routers/serializers/NotifySerializer"; +import { BotStaticRouter } from "@spt-aki/routers/static/BotStaticRouter"; +import { BundleStaticRouter } from "@spt-aki/routers/static/BundleStaticRouter"; +import { ClientLogStaticRouter } from "@spt-aki/routers/static/ClientLogStaticRouter"; +import { CustomizationStaticRouter } from "@spt-aki/routers/static/CustomizationStaticRouter"; +import { DataStaticRouter } from "@spt-aki/routers/static/DataStaticRouter"; +import { DialogStaticRouter } from "@spt-aki/routers/static/DialogStaticRouter"; +import { GameStaticRouter } from "@spt-aki/routers/static/GameStaticRouter"; +import { HealthStaticRouter } from "@spt-aki/routers/static/HealthStaticRouter"; +import { InraidStaticRouter } from "@spt-aki/routers/static/InraidStaticRouter"; +import { InsuranceStaticRouter } from "@spt-aki/routers/static/InsuranceStaticRouter"; +import { ItemEventStaticRouter } from "@spt-aki/routers/static/ItemEventStaticRouter"; +import { LauncherStaticRouter } from "@spt-aki/routers/static/LauncherStaticRouter"; +import { LocationStaticRouter } from "@spt-aki/routers/static/LocationStaticRouter"; +import { MatchStaticRouter } from "@spt-aki/routers/static/MatchStaticRouter"; +import { NotifierStaticRouter } from "@spt-aki/routers/static/NotifierStaticRouter"; +import { PresetStaticRouter } from "@spt-aki/routers/static/PresetStaticRouter"; +import { ProfileStaticRouter } from "@spt-aki/routers/static/ProfileStaticRouter"; +import { QuestStaticRouter } from "@spt-aki/routers/static/QuestStaticRouter"; +import { RagfairStaticRouter } from "@spt-aki/routers/static/RagfairStaticRouter"; +import { TraderStaticRouter } from "@spt-aki/routers/static/TraderStaticRouter"; +import { WeatherStaticRouter } from "@spt-aki/routers/static/WeatherStaticRouter"; +import { ConfigServer } from "@spt-aki/servers/ConfigServer"; +import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; +import { HttpServer } from "@spt-aki/servers/HttpServer"; +import { RagfairServer } from "@spt-aki/servers/RagfairServer"; +import { SaveServer } from "@spt-aki/servers/SaveServer"; +import { WebSocketServer } from "@spt-aki/servers/WebSocketServer"; +import { HttpBufferHandler } from "@spt-aki/servers/http/HttpBufferHandler"; +import { BotEquipmentFilterService } from "@spt-aki/services/BotEquipmentFilterService"; +import { BotEquipmentModPoolService } from "@spt-aki/services/BotEquipmentModPoolService"; +import { BotGenerationCacheService } from "@spt-aki/services/BotGenerationCacheService"; +import { BotLootCacheService } from "@spt-aki/services/BotLootCacheService"; +import { BotWeaponModLimitService } from "@spt-aki/services/BotWeaponModLimitService"; +import { CustomLocationWaveService } from "@spt-aki/services/CustomLocationWaveService"; +import { FenceService } from "@spt-aki/services/FenceService"; +import { GiftService } from "@spt-aki/services/GiftService"; +import { HashCacheService } from "@spt-aki/services/HashCacheService"; +import { InsuranceService } from "@spt-aki/services/InsuranceService"; +import { ItemBaseClassService } from "@spt-aki/services/ItemBaseClassService"; +import { ItemFilterService } from "@spt-aki/services/ItemFilterService"; +import { LocaleService } from "@spt-aki/services/LocaleService"; +import { LocalisationService } from "@spt-aki/services/LocalisationService"; +import { MailSendService } from "@spt-aki/services/MailSendService"; +import { MatchBotDetailsCacheService } from "@spt-aki/services/MatchBotDetailsCacheService"; +import { MatchLocationService } from "@spt-aki/services/MatchLocationService"; +import { ModCompilerService } from "@spt-aki/services/ModCompilerService"; +import { NotificationService } from "@spt-aki/services/NotificationService"; +import { OpenZoneService } from "@spt-aki/services/OpenZoneService"; +import { PaymentService } from "@spt-aki/services/PaymentService"; +import { PlayerService } from "@spt-aki/services/PlayerService"; +import { PmcChatResponseService } from "@spt-aki/services/PmcChatResponseService"; +import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; +import { ProfileSnapshotService } from "@spt-aki/services/ProfileSnapshotService"; +import { RagfairCategoriesService } from "@spt-aki/services/RagfairCategoriesService"; +import { RagfairLinkedItemService } from "@spt-aki/services/RagfairLinkedItemService"; +import { RagfairOfferService } from "@spt-aki/services/RagfairOfferService"; +import { RagfairPriceService } from "@spt-aki/services/RagfairPriceService"; +import { RagfairRequiredItemsService } from "@spt-aki/services/RagfairRequiredItemsService"; +import { RagfairTaxService } from "@spt-aki/services/RagfairTaxService"; +import { RepairService } from "@spt-aki/services/RepairService"; +import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService"; +import { TraderAssortService } from "@spt-aki/services/TraderAssortService"; +import { TraderPurchasePersisterService } from "@spt-aki/services/TraderPurchasePersisterService"; +import { CustomItemService } from "@spt-aki/services/mod/CustomItemService"; +import { DynamicRouterModService } from "@spt-aki/services/mod/dynamicRouter/DynamicRouterModService"; +import { HttpListenerModService } from "@spt-aki/services/mod/httpListener/HttpListenerModService"; +import { ImageRouteService } from "@spt-aki/services/mod/image/ImageRouteService"; +import { OnLoadModService } from "@spt-aki/services/mod/onLoad/OnLoadModService"; +import { OnUpdateModService } from "@spt-aki/services/mod/onUpdate/OnUpdateModService"; +import { StaticRouterModService } from "@spt-aki/services/mod/staticRouter/StaticRouterModService"; +import { App } from "@spt-aki/utils/App"; +import { AsyncQueue } from "@spt-aki/utils/AsyncQueue"; +import { DatabaseImporter } from "@spt-aki/utils/DatabaseImporter"; +import { EncodingUtil } from "@spt-aki/utils/EncodingUtil"; +import { HashUtil } from "@spt-aki/utils/HashUtil"; +import { HttpFileUtil } from "@spt-aki/utils/HttpFileUtil"; +import { HttpResponseUtil } from "@spt-aki/utils/HttpResponseUtil"; +import { ImporterUtil } from "@spt-aki/utils/ImporterUtil"; +import { JsonUtil } from "@spt-aki/utils/JsonUtil"; +import { MathUtil } from "@spt-aki/utils/MathUtil"; +import { ObjectId } from "@spt-aki/utils/ObjectId"; +import { RandomUtil } from "@spt-aki/utils/RandomUtil"; +import { TimeUtil } from "@spt-aki/utils/TimeUtil"; +import { UUidGenerator } from "@spt-aki/utils/UUidGenerator"; +import { VFS } from "@spt-aki/utils/VFS"; +import { Watermark, WatermarkLocale } from "@spt-aki/utils/Watermark"; +import { WinstonMainLogger } from "@spt-aki/utils/logging/WinstonMainLogger"; +import { WinstonRequestLogger } from "@spt-aki/utils/logging/WinstonRequestLogger"; + +// For the Jest Custom Environment. import NodeEnvironment from "jest-environment-node"; import type { EnvironmentContext, JestEnvironmentConfig } from "@jest/environment"; -import { Container } from "@spt-aki/di/Container"; -import { DatabaseImporter } from "@spt-aki/utils/DatabaseImporter"; +// Used for importing the database. +import path from "path"; +import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables"; export default class CustomEnvironment extends NodeEnvironment { + private container: DependencyContainer; + + // For importing the database. + private importerUtil: ImporterUtil; + private databaseServer: DatabaseServer; + constructor(config: JestEnvironmentConfig, context: EnvironmentContext) { super(config, context); + + this.container = this.register(container); } async setup(): Promise { await super.setup(); - await Container.registerTypes(container); + // Import the database. + await this.importDatabase(); - const databaseImporter = container.resolve("DatabaseImporter"); - await databaseImporter.onLoad(); + // Make the container accessible to the tests. + this.global.container = this.container; - this.global.container = container; + // TODO: Create test account/profile } async teardown(): Promise { + // TODO: Delete test account/profile + await super.teardown(); } + + // Register all of the things! + private register(container: DependencyContainer): DependencyContainer + { + container.register("ApplicationContext", ApplicationContext, { lifecycle: Lifecycle.Singleton }); + + this.registerUtils(container); + this.registerRouters(container); + this.registerGenerators(container); + this.registerHelpers(container); + this.registerLoaders(container); + this.registerCallbacks(container); + this.registerServers(container); + this.registerServices(container); + this.registerControllers(container); + + this.registerListTypes(container); + + return container; + } + + private registerUtils(container: DependencyContainer): void + { + // Utils + container.register("App", App, { lifecycle: Lifecycle.Singleton }); + container.register("DatabaseImporter", DatabaseImporter, { lifecycle: Lifecycle.Singleton }); + container.register("HashUtil", HashUtil, { lifecycle: Lifecycle.Singleton }); + container.register("ImporterUtil", ImporterUtil, { lifecycle: Lifecycle.Singleton }); + container.register("HttpResponseUtil", HttpResponseUtil); + container.register("EncodingUtil", EncodingUtil, { lifecycle: Lifecycle.Singleton }); + container.register("JsonUtil", JsonUtil); + container.register("WinstonLogger", WinstonMainLogger, { lifecycle: Lifecycle.Singleton }); + container.register("RequestsLogger", WinstonRequestLogger, { lifecycle: Lifecycle.Singleton }); + container.register("MathUtil", MathUtil, { lifecycle: Lifecycle.Singleton }); + container.register("ObjectId", ObjectId); + container.register("RandomUtil", RandomUtil, { lifecycle: Lifecycle.Singleton }); + container.register("TimeUtil", TimeUtil, { lifecycle: Lifecycle.Singleton }); + container.register("VFS", VFS, { lifecycle: Lifecycle.Singleton }); + container.register("WatermarkLocale", WatermarkLocale, { lifecycle: Lifecycle.Singleton }); + container.register("Watermark", Watermark, { lifecycle: Lifecycle.Singleton }); + container.register("AsyncQueue", AsyncQueue, { lifecycle: Lifecycle.Singleton }); + container.register("UUidGenerator", UUidGenerator, { lifecycle: Lifecycle.Singleton }); + container.register("HttpFileUtil", HttpFileUtil, { lifecycle: Lifecycle.Singleton }); + container.register("ModLoadOrder", ModLoadOrder, { lifecycle: Lifecycle.Singleton }); + container.register("ModTypeCheck", ModTypeCheck, { lifecycle: Lifecycle.Singleton }); + } + + private registerRouters(container: DependencyContainer): void + { + // Routers + container.register("HttpRouter", HttpRouter, { lifecycle: Lifecycle.Singleton }); + container.register("ImageRouter", ImageRouter); + container.register("EventOutputHolder", EventOutputHolder, { lifecycle: Lifecycle.Singleton }); + container.register("ItemEventRouter", ItemEventRouter); + + // Dynamic Routes + container.register("BotDynamicRouter", { useClass: BotDynamicRouter }); + container.register("BundleDynamicRouter", { useClass: BundleDynamicRouter }); + container.register("CustomizationDynamicRouter", { useClass: CustomizationDynamicRouter }); + container.register("DataDynamicRouter", { useClass: DataDynamicRouter }); + container.register("HttpDynamicRouter", { useClass: HttpDynamicRouter }); + container.register("InraidDynamicRouter", { useClass: InraidDynamicRouter }); + container.register("LocationDynamicRouter", { useClass: LocationDynamicRouter }); + container.register("NotifierDynamicRouter", { useClass: NotifierDynamicRouter }); + container.register("TraderDynamicRouter", { useClass: TraderDynamicRouter }); + + // Item Event Routes + container.register("CustomizationItemEventRouter", { useClass: CustomizationItemEventRouter }); + container.register("HealthItemEventRouter", { useClass: HealthItemEventRouter }); + container.register("HideoutItemEventRouter", { useClass: HideoutItemEventRouter }); + container.register("InsuranceItemEventRouter", { useClass: InsuranceItemEventRouter }); + container.register("InventoryItemEventRouter", { useClass: InventoryItemEventRouter }); + container.register("NoteItemEventRouter", { useClass: NoteItemEventRouter }); + container.register("PresetBuildItemEventRouter", { useClass: PresetBuildItemEventRouter }); + container.register("QuestItemEventRouter", { useClass: QuestItemEventRouter }); + container.register("RagfairItemEventRouter", { useClass: RagfairItemEventRouter }); + container.register("RepairItemEventRouter", { useClass: RepairItemEventRouter }); + container.register("TradeItemEventRouter", { useClass: TradeItemEventRouter }); + container.register("WishlistItemEventRouter", { useClass: WishlistItemEventRouter }); + + // Save Load Routes + container.register("HealthSaveLoadRouter", { useClass: HealthSaveLoadRouter }); + container.register("InraidSaveLoadRouter", { useClass: InraidSaveLoadRouter }); + container.register("InsuranceSaveLoadRouter", { useClass: InsuranceSaveLoadRouter }); + container.register("ProfileSaveLoadRouter", { useClass: ProfileSaveLoadRouter }); + + // Route Serializers + container.register("BundleSerializer", { useClass: BundleSerializer }); + container.register("ImageSerializer", { useClass: ImageSerializer }); + container.register("NotifySerializer", { useClass: NotifySerializer }); + + // Static Routes + container.register("BotStaticRouter", { useClass: BotStaticRouter }); + container.register("BundleStaticRouter", { useClass: BundleStaticRouter }); + container.register("ClientLogStaticRouter", { useClass: ClientLogStaticRouter }); + container.register("CustomizationStaticRouter", { useClass: CustomizationStaticRouter }); + container.register("DataStaticRouter", { useClass: DataStaticRouter }); + container.register("DialogStaticRouter", { useClass: DialogStaticRouter }); + container.register("GameStaticRouter", { useClass: GameStaticRouter }); + container.register("HealthStaticRouter", { useClass: HealthStaticRouter }); + container.register("InraidStaticRouter", { useClass: InraidStaticRouter }); + container.register("InsuranceStaticRouter", { useClass: InsuranceStaticRouter }); + container.register("ItemEventStaticRouter", { useClass: ItemEventStaticRouter }); + container.register("LauncherStaticRouter", { useClass: LauncherStaticRouter }); + container.register("LocationStaticRouter", { useClass: LocationStaticRouter }); + container.register("MatchStaticRouter", { useClass: MatchStaticRouter }); + container.register("NotifierStaticRouter", { useClass: NotifierStaticRouter }); + container.register("PresetStaticRouter", { useClass: PresetStaticRouter }); + container.register("ProfileStaticRouter", { useClass: ProfileStaticRouter }); + container.register("QuestStaticRouter", { useClass: QuestStaticRouter }); + container.register("RagfairStaticRouter", { useClass: RagfairStaticRouter }); + container.register("TraderStaticRouter", { useClass: TraderStaticRouter }); + container.register("WeatherStaticRouter", { useClass: WeatherStaticRouter }); + } + + private registerGenerators(container: DependencyContainer): void + { + // Generators + container.register("BotGenerator", BotGenerator); + container.register("BotWeaponGenerator", BotWeaponGenerator); + container.register("BotLootGenerator", BotLootGenerator); + container.register("BotInventoryGenerator", BotInventoryGenerator); + container.register("LocationGenerator", { useClass: LocationGenerator }); + container.register("PMCLootGenerator", PMCLootGenerator, { lifecycle: Lifecycle.Singleton }); + container.register("ScavCaseRewardGenerator", ScavCaseRewardGenerator, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairAssortGenerator", { useClass: RagfairAssortGenerator }); + container.register("RagfairOfferGenerator", { useClass: RagfairOfferGenerator }); + container.register("WeatherGenerator", { useClass: WeatherGenerator }); + container.register("PlayerScavGenerator", { useClass: PlayerScavGenerator }); + container.register("LootGenerator", { useClass: LootGenerator }); + container.register("FenceBaseAssortGenerator", { useClass: FenceBaseAssortGenerator }); + container.register("BotLevelGenerator", { useClass: BotLevelGenerator }); + container.register("BotEquipmentModGenerator", { useClass: BotEquipmentModGenerator }); + container.register("RepeatableQuestGenerator", { useClass: RepeatableQuestGenerator }); + container.register("BarrelInventoryMagGen", { useClass: BarrelInventoryMagGen }); + container.register("ExternalInventoryMagGen", { useClass: ExternalInventoryMagGen }); + container.register("InternalMagazineInventoryMagGen", { useClass: InternalMagazineInventoryMagGen }); + container.register("UbglExternalMagGen", { useClass: UbglExternalMagGen }); + + container.registerType("InventoryMagGen", "BarrelInventoryMagGen"); + container.registerType("InventoryMagGen", "ExternalInventoryMagGen"); + container.registerType("InventoryMagGen", "InternalMagazineInventoryMagGen"); + container.registerType("InventoryMagGen", "UbglExternalMagGen"); + } + + private registerHelpers(container: DependencyContainer): void + { + // Helpers + container.register("AssortHelper", { useClass: AssortHelper }); + container.register("BotHelper", { useClass: BotHelper }); + container.register("BotGeneratorHelper", { useClass: BotGeneratorHelper }); + container.register("ContainerHelper", ContainerHelper); + container.register("DialogueHelper", { useClass: DialogueHelper }); + container.register("DurabilityLimitsHelper", { useClass: DurabilityLimitsHelper }); + container.register("GameEventHelper", GameEventHelper); + container.register("HandbookHelper", HandbookHelper, { lifecycle: Lifecycle.Singleton }); + container.register("HealthHelper", { useClass: HealthHelper }); + container.register("HideoutHelper", { useClass: HideoutHelper }); + container.register("InRaidHelper", { useClass: InRaidHelper }); + container.register("InventoryHelper", { useClass: InventoryHelper }); + container.register("PaymentHelper", PaymentHelper); + container.register("ItemHelper", { useClass: ItemHelper }); + container.register("PresetHelper", PresetHelper, { lifecycle: Lifecycle.Singleton }); + container.register("ProfileHelper", { useClass: ProfileHelper }); + container.register("QuestHelper", { useClass: QuestHelper }); + container.register("QuestConditionHelper", QuestConditionHelper); + container.register("RagfairHelper", { useClass: RagfairHelper }); + container.register("RagfairSortHelper", { useClass: RagfairSortHelper }); + container.register("RagfairSellHelper", { useClass: RagfairSellHelper }); + container.register("RagfairOfferHelper", { useClass: RagfairOfferHelper }); + container.register("RagfairServerHelper", { useClass: RagfairServerHelper }); + container.register("RepairHelper", { useClass: RepairHelper }); + container.register("TraderHelper", TraderHelper); + container.register("TraderAssortHelper", TraderAssortHelper, { lifecycle: Lifecycle.Singleton }); + container.register("TradeHelper", { useClass: TradeHelper }); + container.register("NotifierHelper", { useClass: NotifierHelper }); + container.register("UtilityHelper", UtilityHelper); + container.register("WeightedRandomHelper", { useClass: WeightedRandomHelper }); + container.register("HttpServerHelper", { useClass: HttpServerHelper }); + container.register("NotificationSendHelper", { useClass: NotificationSendHelper }); + container.register("SecureContainerHelper", { useClass: SecureContainerHelper }); + container.register("ProbabilityHelper", { useClass: ProbabilityHelper }); + container.register("BotWeaponGeneratorHelper", { useClass: BotWeaponGeneratorHelper }); + container.register("BotDifficultyHelper", { useClass: BotDifficultyHelper }); + container.register("RepeatableQuestHelper", { useClass: RepeatableQuestHelper }); + } + + private registerLoaders(container: DependencyContainer): void + { + // Loaders + container.register("BundleLoader", BundleLoader, { lifecycle: Lifecycle.Singleton }); + container.register("PreAkiModLoader", PreAkiModLoader, { lifecycle: Lifecycle.Singleton }); + container.register("PostAkiModLoader", PostAkiModLoader, { lifecycle: Lifecycle.Singleton }); + } + + private registerCallbacks(container: DependencyContainer): void + { + // Callbacks + container.register("BotCallbacks", { useClass: BotCallbacks }); + container.register("BundleCallbacks", { useClass: BundleCallbacks }); + container.register("ClientLogCallbacks", { useClass: ClientLogCallbacks }); + container.register("CustomizationCallbacks", { useClass: CustomizationCallbacks }); + container.register("DataCallbacks", { useClass: DataCallbacks }); + container.register("DialogueCallbacks", { useClass: DialogueCallbacks }); + container.register("GameCallbacks", { useClass: GameCallbacks }); + container.register("HandbookCallbacks", { useClass: HandbookCallbacks }); + container.register("HealthCallbacks", { useClass: HealthCallbacks }); + container.register("HideoutCallbacks", { useClass: HideoutCallbacks }); + container.register("HttpCallbacks", { useClass: HttpCallbacks }); + container.register("InraidCallbacks", { useClass: InraidCallbacks }); + container.register("InsuranceCallbacks", { useClass: InsuranceCallbacks }); + container.register("InventoryCallbacks", { useClass: InventoryCallbacks }); + container.register("ItemEventCallbacks", { useClass: ItemEventCallbacks }); + container.register("LauncherCallbacks", { useClass: LauncherCallbacks }); + container.register("LocationCallbacks", { useClass: LocationCallbacks }); + container.register("MatchCallbacks", { useClass: MatchCallbacks }); + container.register("ModCallbacks", { useClass: ModCallbacks }); + container.register("PostDBModLoader", { useClass: PostDBModLoader }); + container.register("NoteCallbacks", { useClass: NoteCallbacks }); + container.register("NotifierCallbacks", { useClass: NotifierCallbacks }); + container.register("PresetBuildCallbacks", { useClass: PresetBuildCallbacks }); + container.register("PresetCallbacks", { useClass: PresetCallbacks }); + container.register("ProfileCallbacks", { useClass: ProfileCallbacks }); + container.register("QuestCallbacks", { useClass: QuestCallbacks }); + container.register("RagfairCallbacks", { useClass: RagfairCallbacks }); + container.register("RepairCallbacks", { useClass: RepairCallbacks }); + container.register("SaveCallbacks", { useClass: SaveCallbacks }); + container.register("TradeCallbacks", { useClass: TradeCallbacks }); + container.register("TraderCallbacks", { useClass: TraderCallbacks }); + container.register("WeatherCallbacks", { useClass: WeatherCallbacks }); + container.register("WishlistCallbacks", { useClass: WishlistCallbacks }); + } + + private registerServices(container: DependencyContainer): void + { + // Services + container.register("ImageRouteService", ImageRouteService, { lifecycle: Lifecycle.Singleton }); + container.register("FenceService", FenceService, { lifecycle: Lifecycle.Singleton }); + container.register("PlayerService", { useClass: PlayerService }); + container.register("PaymentService", { useClass: PaymentService }); + container.register("InsuranceService", InsuranceService, { lifecycle: Lifecycle.Singleton }); + container.register("TraderAssortService", TraderAssortService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairPriceService", RagfairPriceService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairCategoriesService", RagfairCategoriesService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairOfferService", RagfairOfferService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairLinkedItemService", RagfairLinkedItemService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairRequiredItemsService", RagfairRequiredItemsService, { lifecycle: Lifecycle.Singleton }); + container.register("NotificationService", NotificationService, { lifecycle: Lifecycle.Singleton }); + container.register("MatchLocationService", MatchLocationService, { lifecycle: Lifecycle.Singleton }); + container.register("ModCompilerService", ModCompilerService); + container.register("HashCacheService", HashCacheService, { lifecycle: Lifecycle.Singleton }); + container.register("LocaleService", LocaleService, { lifecycle: Lifecycle.Singleton }); + container.register("ProfileFixerService", ProfileFixerService); + container.register("RepairService", RepairService); + container.register("BotLootCacheService", BotLootCacheService, { lifecycle: Lifecycle.Singleton }); + container.register("CustomItemService", CustomItemService); + container.register("BotEquipmentFilterService", BotEquipmentFilterService); + container.register("ProfileSnapshotService", ProfileSnapshotService, { lifecycle: Lifecycle.Singleton }); + container.register("ItemFilterService", ItemFilterService, { lifecycle: Lifecycle.Singleton }); + container.register("BotGenerationCacheService", BotGenerationCacheService, { lifecycle: Lifecycle.Singleton }); + container.register("LocalisationService", LocalisationService, { lifecycle: Lifecycle.Singleton }); + container.register("CustomLocationWaveService", CustomLocationWaveService, { lifecycle: Lifecycle.Singleton }); + container.register("OpenZoneService", OpenZoneService, { lifecycle: Lifecycle.Singleton }); + container.register("ItemBaseClassService", ItemBaseClassService, { lifecycle: Lifecycle.Singleton }); + container.register("BotEquipmentModPoolService", BotEquipmentModPoolService, { lifecycle: Lifecycle.Singleton }); + container.register("BotWeaponModLimitService", BotWeaponModLimitService, { lifecycle: Lifecycle.Singleton }); + container.register("SeasonalEventService", SeasonalEventService, { lifecycle: Lifecycle.Singleton }); + container.register("MatchBotDetailsCacheService", MatchBotDetailsCacheService, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairTaxService", RagfairTaxService, { lifecycle: Lifecycle.Singleton }); + container.register("TraderPurchasePersisterService", TraderPurchasePersisterService); + container.register("PmcChatResponseService", PmcChatResponseService); + container.register("GiftService", GiftService); + container.register("MailSendService", MailSendService); + } + + private registerServers(container: DependencyContainer): void + { + // Servers + container.register("DatabaseServer", DatabaseServer, { lifecycle: Lifecycle.Singleton }); + container.register("HttpServer", HttpServer, { lifecycle: Lifecycle.Singleton }); + container.register("WebSocketServer", WebSocketServer, { lifecycle: Lifecycle.Singleton }); + container.register("RagfairServer", RagfairServer); + container.register("SaveServer", SaveServer, { lifecycle: Lifecycle.Singleton }); + container.register("ConfigServer", ConfigServer, { lifecycle: Lifecycle.Singleton }); + container.register("HttpBufferHandler", HttpBufferHandler, {lifecycle: Lifecycle.Singleton}); + } + + private registerControllers(container: DependencyContainer): void + { + // Controllers + container.register("BotController", { useClass: BotController }); + container.register("ClientLogController", { useClass: ClientLogController }); + container.register("CustomizationController", { useClass: CustomizationController }); + container.register("DialogueController", { useClass: DialogueController }); + container.register("GameController", { useClass: GameController }); + container.register("HandbookController", { useClass: HandbookController }); + container.register("HealthController", { useClass: HealthController }); + container.register("HideoutController", { useClass: HideoutController }); + container.register("InraidController", { useClass: InraidController }); + container.register("InsuranceController", { useClass: InsuranceController }); + container.register("InventoryController", { useClass: InventoryController }); + container.register("LauncherController", { useClass: LauncherController }); + container.register("LocationController", { useClass: LocationController }); + container.register("MatchController", MatchController); + container.register("NoteController", { useClass: NoteController }); + container.register("NotifierController", { useClass: NotifierController }); + container.register("PresetBuildController", { useClass: PresetBuildController }); + container.register("PresetController", { useClass: PresetController }); + container.register("ProfileController", { useClass: ProfileController }); + container.register("QuestController", { useClass: QuestController }); + container.register("RagfairController", { useClass: RagfairController }); + container.register("RepairController", { useClass: RepairController }); + container.register("RepeatableQuestController", { useClass: RepeatableQuestController }); + container.register("TradeController", { useClass: TradeController }); + container.register("TraderController", { useClass: TraderController }); + container.register("WeatherController", { useClass: WeatherController }); + container.register("WishlistController", WishlistController); + } + + private registerListTypes(container: DependencyContainer): void + { + container.register("OnLoadModService", { useValue: new OnLoadModService(container) }); + container.register("HttpListenerModService", { useValue: new HttpListenerModService(container) }); + container.register("OnUpdateModService", { useValue: new OnUpdateModService(container) }); + container.register("DynamicRouterModService", { useValue: new DynamicRouterModService(container) }); + container.register("StaticRouterModService", { useValue: new StaticRouterModService(container) }); + + container.registerType("OnLoad", "DatabaseImporter"); + container.registerType("OnLoad", "PostDBModLoader"); + container.registerType("OnLoad", "HandbookCallbacks"); + container.registerType("OnLoad", "HttpCallbacks"); + container.registerType("OnLoad", "PresetCallbacks"); + container.registerType("OnLoad", "SaveCallbacks"); + container.registerType("OnLoad", "TraderCallbacks"); // must occur prior to RagfairCallbacks + container.registerType("OnLoad", "RagfairPriceService"); + container.registerType("OnLoad", "RagfairCallbacks"); + container.registerType("OnLoad", "ModCallbacks"); + container.registerType("OnLoad", "GameCallbacks"); + + container.registerType("OnUpdate", "DialogueCallbacks"); + container.registerType("OnUpdate", "HideoutCallbacks"); + container.registerType("OnUpdate", "TraderCallbacks"); + container.registerType("OnUpdate", "RagfairCallbacks"); + container.registerType("OnUpdate", "InsuranceCallbacks"); + container.registerType("OnUpdate", "SaveCallbacks"); + + container.registerType("StaticRoutes", "BotStaticRouter"); + container.registerType("StaticRoutes", "ClientLogStaticRouter"); + container.registerType("StaticRoutes", "CustomizationStaticRouter"); + container.registerType("StaticRoutes", "DataStaticRouter"); + container.registerType("StaticRoutes", "DialogStaticRouter"); + container.registerType("StaticRoutes", "GameStaticRouter"); + container.registerType("StaticRoutes", "HealthStaticRouter"); + container.registerType("StaticRoutes", "InraidStaticRouter"); + container.registerType("StaticRoutes", "InsuranceStaticRouter"); + container.registerType("StaticRoutes", "ItemEventStaticRouter"); + container.registerType("StaticRoutes", "LauncherStaticRouter"); + container.registerType("StaticRoutes", "LocationStaticRouter"); + container.registerType("StaticRoutes", "WeatherStaticRouter"); + container.registerType("StaticRoutes", "MatchStaticRouter"); + container.registerType("StaticRoutes", "QuestStaticRouter"); + container.registerType("StaticRoutes", "RagfairStaticRouter"); + container.registerType("StaticRoutes", "PresetStaticRouter"); + container.registerType("StaticRoutes", "BundleStaticRouter"); + container.registerType("StaticRoutes", "NotifierStaticRouter"); + container.registerType("StaticRoutes", "ProfileStaticRouter"); + container.registerType("StaticRoutes", "TraderStaticRouter"); + + container.registerType("DynamicRoutes", "BotDynamicRouter"); + container.registerType("DynamicRoutes", "BundleDynamicRouter"); + container.registerType("DynamicRoutes", "CustomizationDynamicRouter"); + container.registerType("DynamicRoutes", "DataDynamicRouter"); + container.registerType("DynamicRoutes", "HttpDynamicRouter"); + container.registerType("DynamicRoutes", "InraidDynamicRouter"); + container.registerType("DynamicRoutes", "LocationDynamicRouter"); + container.registerType("DynamicRoutes", "NotifierDynamicRouter"); + container.registerType("DynamicRoutes", "TraderDynamicRouter"); + + container.registerType("IERouters", "CustomizationItemEventRouter"); + container.registerType("IERouters", "HealthItemEventRouter"); + container.registerType("IERouters", "HideoutItemEventRouter"); + container.registerType("IERouters", "InsuranceItemEventRouter"); + container.registerType("IERouters", "InventoryItemEventRouter"); + container.registerType("IERouters", "NoteItemEventRouter"); + container.registerType("IERouters", "PresetBuildItemEventRouter"); + container.registerType("IERouters", "QuestItemEventRouter"); + container.registerType("IERouters", "RagfairItemEventRouter"); + container.registerType("IERouters", "RepairItemEventRouter"); + container.registerType("IERouters", "TradeItemEventRouter"); + container.registerType("IERouters", "WishlistItemEventRouter"); + + container.registerType("Serializer", "ImageSerializer"); + container.registerType("Serializer", "BundleSerializer"); + container.registerType("Serializer", "NotifySerializer"); + + // Registering these starts the server...? + container.registerType("SaveLoadRouter", "HealthSaveLoadRouter"); + container.registerType("SaveLoadRouter", "InraidSaveLoadRouter"); + container.registerType("SaveLoadRouter", "InsuranceSaveLoadRouter"); + container.registerType("SaveLoadRouter", "ProfileSaveLoadRouter"); + } + + private async importDatabase(): Promise + { + this.importerUtil = this.container.resolve("ImporterUtil"); + this.databaseServer = this.container.resolve("DatabaseServer"); + + // Read the data from the JSON files. + const databaseDir = path.resolve("./assets/database"); + const dataToImport = await this.importerUtil.loadAsync(`${databaseDir}/`); + + // Save the data to memory. + this.databaseServer.setTables(dataToImport); + } } diff --git a/project/tests/helpers/ItemHelper.test.ts b/project/tests/helpers/ItemHelper.test.ts index 4a4e2edc..e74f4bc2 100644 --- a/project/tests/helpers/ItemHelper.test.ts +++ b/project/tests/helpers/ItemHelper.test.ts @@ -2,12 +2,22 @@ import "reflect-metadata"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { DependencyContainer } from "tsyringe"; +import { Item, Repairable } from "@spt-aki/models/eft/common/tables/IItem"; +import { DatabaseServer } from "@spt-aki/servers/DatabaseServer"; +import { HashUtil } from "@spt-aki/utils/HashUtil"; describe("ItemHelper", () => { let container: DependencyContainer; let itemHelper: ItemHelper; + // Spies + let handbookHelperGetTemplatePriceSpy: jest.SpyInstance; + let loggerWarningSpy: jest.SpyInstance; + let loggerErrorSpy: jest.SpyInstance; + let databaseServerGetTablesSpy: jest.SpyInstance; + let jsonUtilCloneSpy: jest.SpyInstance; + beforeAll(() => { container = globalThis.container; @@ -26,5 +36,740 @@ describe("ItemHelper", () => const result = itemHelper.isValidItem("non-existent-item"); expect(result).toBe(false); }); + + it("should return false when item is a quest item", () => + { + const result = itemHelper.isValidItem("590de92486f77423d9312a33"); // "Gold pocket watch on a chain" + expect(result).toBe(false); + }); + + it("should return false when item is of an invalid base type", () => + { + const result = itemHelper.isValidItem("5fc64ea372b0dd78d51159dc", ["invalid-base-type"]); // "Cultist knife" + expect(result).toBe(false); + }); + + it("should return false when item's price is zero", () => + { + // Unsure if any item has price of "0", so mock the getItemPrice method to return 0. + jest.spyOn(itemHelper, "getItemPrice").mockReturnValue(0); + + const result = itemHelper.isValidItem("5fc64ea372b0dd78d51159dc"); + expect(result).toBe(false); + }); + + it("should return false when item is in the blacklist", () => + { + const result = itemHelper.isValidItem("6087e570b998180e9f76dc24"); // "Superfors DB 2020 Dead Blow Hammer" + expect(result).toBe(false); + }); + + it("should return true when item is valid", () => + { + const result = itemHelper.isValidItem("5fc64ea372b0dd78d51159dc"); // "Cultist knife" + expect(result).toBe(true); + }); + }); + + describe("isOfBaseclass", () => + { + it("should return true when item has the given base class", () => + { + // ID 590c657e86f77412b013051d is a "Grizzly medical kit" of base class "MedKit". + const result = itemHelper.isOfBaseclass("590c657e86f77412b013051d", "5448f39d4bdc2d0a728b4568"); + expect(result).toBe(true); + }); + + it("should return false when item does not have the given base class", () => + { + // ID 590c657e86f77412b013051d is a "Grizzly medical kit" not of base class "Knife". + const result = itemHelper.isOfBaseclass("590c657e86f77412b013051d", "5447e1d04bdc2dff2f8b4567"); + expect(result).toBe(false); + }); + }); + + describe("isOfBaseclasses", () => + { + it("should return true when item has the given base class", () => + { + // ID 590c657e86f77412b013051d is a "Grizzly medical kit" of base class "MedKit". + const result = itemHelper.isOfBaseclasses("590c657e86f77412b013051d", ["5448f39d4bdc2d0a728b4568"]); + expect(result).toBe(true); + }); + + it("should return false when item does not have the given base class", () => + { + // ID 590c657e86f77412b013051d is a "Grizzly medical kit" not of base class "Knife". + const result = itemHelper.isOfBaseclasses("590c657e86f77412b013051d", ["5447e1d04bdc2dff2f8b4567"]); + expect(result).toBe(false); + }); + }); + + describe("getItemPrice", () => + { + it("should return static price when it is greater than or equal to 1", () => + { + const staticPrice = 1; + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(staticPrice); + + const result = itemHelper.getItemPrice(tpl); + + expect(result).toBe(staticPrice); + }); + + it("should return dynamic price when static price is less than 1", () => + { + const staticPrice = 0; + const dynamicPrice = 42069; + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(staticPrice); + jest.spyOn(itemHelper, "getDynamicItemPrice").mockReturnValue(dynamicPrice); + + const result = itemHelper.getItemPrice(tpl); + + // Failing because getDynamicItemPrice is called incorrectly. + expect(result).toBe(dynamicPrice); + }); + + it("should return 0 when neither handbook nor dynamic price is available", () => + { + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(0); + jest.spyOn(itemHelper, "getDynamicItemPrice").mockReturnValue(0); + + const result = itemHelper.getItemPrice(tpl); + + // Failing because getStaticItemPrice will return 1 on a failed lookup. ??? + expect(result).toBe(0); + }); + }); + + describe("getItemMaxPrice", () => + { + it("should return static price when it is higher", () => + { + const staticPrice = 420; + const dynamicPrice = 69; + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(staticPrice); + jest.spyOn(itemHelper, "getDynamicItemPrice").mockReturnValue(dynamicPrice); + + const result = itemHelper.getItemMaxPrice(tpl); + + expect(result).toBe(staticPrice); + }); + + it("should return dynamic price when it is higher", () => + { + const staticPrice = 69; + const dynamicPrice = 420; + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(staticPrice); + jest.spyOn(itemHelper, "getDynamicItemPrice").mockReturnValue(dynamicPrice); + + const result = itemHelper.getItemMaxPrice(tpl); + + expect(result).toBe(dynamicPrice); + }); + + it("should return either when both prices are equal", () => + { + const price = 42069; + const tpl = "590c657e86f77412b013051d"; + + jest.spyOn(itemHelper, "getStaticItemPrice").mockReturnValue(price); + jest.spyOn(itemHelper, "getDynamicItemPrice").mockReturnValue(price); + + const result = itemHelper.getItemMaxPrice(tpl); + + expect(result).toBe(price); + }); + + it("should return 0 when item does not exist", () => + { + const tpl = "non-existent-item"; + + const result = itemHelper.getItemMaxPrice(tpl); + + // Failing because getStaticItemPrice will return 1 on a failed lookup. ??? + expect(result).toBe(0); + }); + }); + + describe("getStaticItemPrice", () => + { + it("should return handbook price when it is greater than or equal to 1", () => + { + const price = 42069; + const tpl = "590c657e86f77412b013051d"; + + handbookHelperGetTemplatePriceSpy = jest.spyOn((itemHelper as any).handbookHelper, "getTemplatePrice"); + handbookHelperGetTemplatePriceSpy.mockReturnValue(price); + + const result = itemHelper.getStaticItemPrice(tpl); + + expect(result).toBe(price); + }); + + it("should return 0 when handbook price is less than 1", () => + { + const price = 0; + const tpl = "590c657e86f77412b013051d"; // "Grizzly medical kit" + + handbookHelperGetTemplatePriceSpy = jest.spyOn((itemHelper as any).handbookHelper, "getTemplatePrice"); + handbookHelperGetTemplatePriceSpy.mockReturnValue(price); + + const result = itemHelper.getStaticItemPrice(tpl); + + expect(result).toBe(price); + }); + }); + + describe("getDynamicItemPrice", () => + { + it("should return the correct dynamic price when it exists", () => + { + const tpl = "590c657e86f77412b013051d"; // "Grizzly medical kit" + + const result = itemHelper.getDynamicItemPrice(tpl); + + expect(result).toBeGreaterThanOrEqual(1); + }); + + it("should return 0 when the dynamic price does not exist", () => + { + const tpl = "non-existent-item"; + + const result = itemHelper.getDynamicItemPrice(tpl); + + expect(result).toBe(0); + }); + }); + + describe("fixItemStackCount", () => + { + it("should set upd.StackObjectsCount to 1 if upd is undefined", () => + { + const initialItem: Item = { + _id: "", + _tpl: "" + }; + const fixedItem = itemHelper.fixItemStackCount(initialItem); + + expect(fixedItem.upd).toBeDefined(); + expect(fixedItem.upd!.StackObjectsCount).toBe(1); + }); + + it("should set upd.StackObjectsCount to 1 if upd.StackObjectsCount is undefined", () => + { + const initialItem: Item = { + _id: "", + _tpl: "", + upd: {} + }; + const fixedItem = itemHelper.fixItemStackCount(initialItem); + + expect(fixedItem.upd).toBeDefined(); + expect(fixedItem.upd!.StackObjectsCount).toBe(1); + }); + + it("should not change upd.StackObjectsCount if it is already defined", () => + { + const initialItem: Item = { + _id: "", + _tpl: "", + upd: { + StackObjectsCount: 5 + } + }; + const fixedItem = itemHelper.fixItemStackCount(initialItem); + + expect(fixedItem.upd).toBeDefined(); + expect(fixedItem.upd!.StackObjectsCount).toBe(5); + }); + }); + + describe("generateItemsFromStackSlot", () => + { + it("should generate valid StackSlot item for an AmmoBox", () => + { + const ammoBox = itemHelper.getItem("57372c89245977685d4159b1"); // "5.45x39mm BT gs ammo pack (30 pcs)" + const parentId = container.resolve("HashUtil").generate(); + + const result = itemHelper.generateItemsFromStackSlot(ammoBox[1], parentId); + + expect(result.length).toBe(1); + expect(result[0]._id).toBeDefined(); + expect(result[0]._tpl).toBe(ammoBox[1]._props.StackSlots[0]._props.filters[0].Filter[0]); + expect(result[0].parentId).toBe(parentId); + expect(result[0].slotId).toBe("cartridges"); + expect(result[0].location).toBe(0); + expect(result[0].upd.StackObjectsCount).toBe(ammoBox[1]._props.StackSlots[0]._max_count); + }); + + it("should log a warning if no IDs are found in Filter", () => + { + const ammoBox = itemHelper.getItem("57372c89245977685d4159b1"); // "5.45x39mm BT gs ammo pack (30 pcs)" + ammoBox[1]._props.StackSlots[0]._props.filters[0].Filter = []; // Empty the Filter array. + + const parentId = container.resolve("HashUtil").generate(); + + loggerWarningSpy = jest.spyOn((itemHelper as any).logger, "warning"); + + itemHelper.generateItemsFromStackSlot(ammoBox[1], parentId); + + expect(loggerWarningSpy).toHaveBeenCalled(); + }); + }); + + describe("getItems", () => + { + it("should call databaseServer.getTables() and jsonUtil.clone() methods", () => + { + databaseServerGetTablesSpy = jest.spyOn((itemHelper as any).databaseServer, "getTables"); + jsonUtilCloneSpy = jest.spyOn((itemHelper as any).jsonUtil, "clone"); + + itemHelper.getItems(); + + expect(databaseServerGetTablesSpy).toHaveBeenCalled(); + expect(jsonUtilCloneSpy).toHaveBeenCalled(); + }); + + it("should return a new array, not a reference to the original", () => + { + const tables = container.resolve("DatabaseServer").getTables(); + const originalItems = Object.values(tables.templates.items); + + const clonedItems = itemHelper.getItems(); + + // Change something in the cloned array + clonedItems[0]._id = "modified"; + + // Validate that the original array remains unchanged + expect(originalItems[0]._id).not.toBe("modified"); + }); + }); + + describe("getItem", () => + { + it("should return true and the item if the tpl exists", () => + { + // ID 590c657e86f77412b013051d is a "Grizzly medical kit". + const tpl = "590c657e86f77412b013051d"; + const tables = container.resolve("DatabaseServer").getTables(); + const item = tables.templates.items[tpl]; + + const [isValid, returnedItem] = itemHelper.getItem(tpl); + + expect(isValid).toBe(true); + expect(returnedItem).toBe(item); + }); + + it("should return false and undefined if the tpl does not exist", () => + { + const tpl = "non-existent-item"; + + const [isValid, returnedItem] = itemHelper.getItem(tpl); + + expect(isValid).toBe(false); + expect(returnedItem).toBeUndefined(); + }); + }); + + describe("isItemInDb", () => + { + it("should return true if getItem returns true as the first element", () => + { + const tpl = "590c657e86f77412b013051d"; // "Grizzly medical kit" + + const result = itemHelper.isItemInDb(tpl); + + expect(result).toBe(true); + }); + + it("should return false if getItem returns false as the first element", () => + { + const tpl = "non-existent-item"; + + const result = itemHelper.isItemInDb(tpl); + + expect(result).toBe(false); + }); + + it("should call getItem with the provided tpl", () => + { + const itemHelperSpy = jest.spyOn(itemHelper, "getItem"); + + const tpl = "590c657e86f77412b013051d"; // "Grizzly medical kit" + + itemHelper.isItemInDb(tpl); + + expect(itemHelperSpy).toHaveBeenCalledWith(tpl); + }); + }); + + describe("getItemQualityModifier", () => + { + it("should return 1 for an item with no upd", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "590c657e86f77412b013051d" // "Grizzly medical kit" + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(1); + }); + + it("should return 1 for an item with upd but no relevant fields", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "590c657e86f77412b013051d", // "Grizzly medical kit" + upd: {} + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(1); + }); + + it("should return correct value for a medkit", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "590c657e86f77412b013051d", // "Grizzly medical kit" + upd: { + MedKit: { + HpResource: 900 // 1800 total + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return correct value for a reparable helmet", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "5b40e1525acfc4771e1c6611", // "HighCom Striker ULACH IIIA helmet (Black)" + upd: { + Repairable: { + Durability: 19, + MaxDurability: 38 + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return correct value for a reparable weapon", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "5a38e6bac4a2826c6e06d79b", // "TOZ-106 20ga bolt-action shotgun" + upd: { + Repairable: { + Durability: 20, + MaxDurability: 100 + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBeCloseTo(0.447); + }); + + it("should return correct value for a food or drink item", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "5448fee04bdc2dbc018b4567", // "Bottle of water (0.6L)" + upd: { + FoodDrink: { + HpPercent: 30 // Not actually a percentage, but value of max 60. + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return correct value for a key item", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "5780cf7f2459777de4559322", // "Dorm room 314 marked key" + upd: { + Key: { + NumberOfUsages: 5 + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return correct value for a resource item", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "5d1b36a186f7742523398433", // "Metal fuel tank" + upd: { + Resource: { + Value: 50, // How much fuel is left in the tank. + UnitsConsumed: 50 // How much fuel has been used in the generator. + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return correct value for a repair kit item", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "591094e086f7747caa7bb2ef", // "Body armor repair kit" + upd: { + RepairKit: { + Resource: 600 + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.5); + }); + + it("should return 0.01 for an item with upd but all relevant fields are 0", () => + { + const itemId = container.resolve("HashUtil").generate(); + const item: Item = { + _id: itemId, + _tpl: "591094e086f7747caa7bb2ef", // "Body armor repair kit" + upd: { + RepairKit: { + Resource: 0 + } + } + }; + + const result = itemHelper.getItemQualityModifier(item); + + expect(result).toBe(0.01); + }); + }); + + describe("getRepairableItemQualityValue", () => + { + it("should return the correct quality value for armor items", () => + { + const armor = itemHelper.getItem("5648a7494bdc2d9d488b4583")[1]; // "PACA Soft Armor" + const repairable: Repairable = { + Durability: 25, + MaxDurability: 50 + }; + const item: Item = { // Not used for armor, but required for the method. + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(armor, repairable, item); + + expect(result).toBe(0.5); + }); + + it("should not use the Repairable MaxDurability property for armor", () => + { + const armor = itemHelper.getItem("5648a7494bdc2d9d488b4583")[1]; // "PACA Soft Armor" + const repairable: Repairable = { + Durability: 25, + MaxDurability: 1000 // This should be ignored. + }; + const item: Item = { // Not used for armor, but required for the method. + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(armor, repairable, item); + + expect(result).toBe(0.5); + }); + + it("should return the correct quality value for weapon items", () => + { + const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" + const repairable: Repairable = { + Durability: 50, + MaxDurability: 100 + }; + const item: Item = { + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(weapon, repairable, item); + + expect(result).toBe(Math.sqrt(0.5)); + }); + + it("should fall back to using Repairable MaxDurability for weapon items", () => + { + const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" + weapon._props.MaxDurability = undefined; // Remove the MaxDurability property. + const repairable: Repairable = { + Durability: 50, + MaxDurability: 200 // This should be used now. + }; + const item: Item = { + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(weapon, repairable, item); + + expect(result).toBe(Math.sqrt(0.25)); + }); + + it("should return 1 if durability value is invalid", () => + { + const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" + weapon._props.MaxDurability = undefined; // Remove the MaxDurability property. + const repairable: Repairable = { + Durability: 50, + MaxDurability: undefined // Remove the MaxDurability property value... Technically an invalid Type. + }; + const item: Item = { + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(weapon, repairable, item); + + expect(result).toBe(1); + }); + + it("should not divide by zero", () => + { + const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" + weapon._props.MaxDurability = undefined; // Remove the MaxDurability property. + const repairable: Repairable = { + Durability: 50, + MaxDurability: 0 // This is a problem. + }; + const item: Item = { + _id: "", + _tpl: "" + }; + + // Cast the method to any to allow access to private/protected method. + const result = (itemHelper as any).getRepairableItemQualityValue(weapon, repairable, item); + + expect(result).toBe(1); + }); + + it("should log an error if durability is invalid", () => + { + const weapon = itemHelper.getItem("5a38e6bac4a2826c6e06d79b")[1]; // "TOZ-106 20ga bolt-action shotgun" + weapon._props.MaxDurability = undefined; // Remove the MaxDurability property. + const repairable: Repairable = { + Durability: 50, + MaxDurability: undefined // Remove the MaxDurability property value... Technically an invalid Type. + }; + const item: Item = { + _id: "", + _tpl: "" + }; + + loggerErrorSpy = jest.spyOn((itemHelper as any).logger, "error"); + + // Cast the method to any to allow access to private/protected method. + (itemHelper as any).getRepairableItemQualityValue(weapon, repairable, item); + + expect(loggerErrorSpy).toBeCalled(); + }); + }); + + describe("findAndReturnChildrenByItems", () => + { + it("should return an array containing only the parent ID when no children are found", () => + { + const items: Item[] = [ + { _id: "1", _tpl: "", parentId: null }, + { _id: "2", _tpl: "", parentId: null }, + { _id: "3", _tpl: "", parentId: "2" } + ]; + const result = itemHelper.findAndReturnChildrenByItems(items, "1"); + expect(result).toEqual(["1"]); + }); + + it("should return array of child IDs when single-level children are found", () => + { + const items: Item[] = [ + { _id: "1", _tpl: "", parentId: null }, + { _id: "2", _tpl: "", parentId: "1" }, + { _id: "3", _tpl: "", parentId: "1" } + ]; + const result = itemHelper.findAndReturnChildrenByItems(items, "1"); + expect(result).toEqual(["2", "3", "1"]); + }); + + it("should return array of child IDs when multi-level children are found", () => + { + const items: Item[] = [ + { _id: "1", _tpl: "", parentId: null }, + { _id: "2", _tpl: "", parentId: "1" }, + { _id: "3", _tpl: "", parentId: "2" }, + { _id: "4", _tpl: "", parentId: "3" } + ]; + const result = itemHelper.findAndReturnChildrenByItems(items, "1"); + expect(result).toEqual(["4", "3", "2", "1"]); + }); + + it("should return an array containing only the parent ID when parent ID does not exist in items", () => + { + const items: Item[] = [ + { _id: "1", _tpl: "", parentId: null }, + { _id: "2", _tpl: "", parentId: "1" } + ]; + const result = itemHelper.findAndReturnChildrenByItems(items, "3"); + expect(result).toEqual(["3"]); + }); }); }); diff --git a/project/tests/services/PaymentService.test.ts b/project/tests/services/PaymentService.test.ts new file mode 100644 index 00000000..485ee80b --- /dev/null +++ b/project/tests/services/PaymentService.test.ts @@ -0,0 +1,30 @@ +import "reflect-metadata"; +import { DependencyContainer } from "tsyringe"; + +import { PaymentService } from "@spt-aki/services/PaymentService"; + +describe("PaymentService", () => +{ + let container: DependencyContainer; + let paymentService: PaymentService; + + beforeAll(() => + { + container = globalThis.container; + paymentService = container.resolve("PaymentService"); + }); + + afterEach(() => + { + jest.restoreAllMocks(); + }); + + describe("should be registered", () => + { + it("should be registered", () => + { + expect(paymentService).toBeDefined(); + expect(container.isRegistered("PaymentService")).toBe(true); + }); + }); +});