diff --git a/.gitignore b/.gitignore index f599662..eed4645 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ install/* !install/*.template.yaml config/localhost.yaml mydb.db-journal +test_db.db-journal diff --git a/src/database/model/building-production-rate.model.ts b/src/database/model/building-production-rate.model.ts index de03167..5963034 100644 --- a/src/database/model/building-production-rate.model.ts +++ b/src/database/model/building-production-rate.model.ts @@ -46,8 +46,10 @@ export class BuildingProductionRateModel { @Column({name: 'resourceId'}) public resourceId: string; + @Field({description: 'Current level production rate in seconds'}) @IsNumber() @Column() + @Column({type: 'float', precision: 10, scale: 4}) public productionRate: number; } diff --git a/src/database/repository/abstract.repository.ts b/src/database/repository/abstract.repository.ts index 85e4921..a29d581 100644 --- a/src/database/repository/abstract.repository.ts +++ b/src/database/repository/abstract.repository.ts @@ -1,5 +1,5 @@ import {Inject, Injectable} from '@nestjs/common'; -import {EntitySubscriberInterface, Repository} from 'typeorm'; +import {Repository} from 'typeorm'; import {TransactionManagerService} from '@warp-core/database/transaction-manager.service'; @@ -10,11 +10,6 @@ export abstract class AbstractRepository< @Inject(TransactionManagerService) private readonly transactionManager: TransactionManagerService; - private static disabledEntityListeners: Map< - object | string, - EntitySubscriberInterface - > = new Map(); - /** * Creates shared transaction. * Shared transaction allows using transactions in different modules @@ -37,50 +32,4 @@ export abstract class AbstractRepository< public rollbackTransaction(): Promise { return this.transactionManager.rollbackTransaction(); } - - public disableEntityListeners( - entityType: object | object[] | string | string[], - ): void { - const entityTypesToCheck = Array.isArray(entityType) - ? entityType - : [entityType]; - - const subscriber = this.manager.connection.subscribers; - for (let i = subscriber.length - 1; i >= 0; --i) { - const subscriberElement = subscriber[i]; - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - if (entityTypesToCheck.includes(subscriberElement.listenTo())) { - AbstractRepository.disabledEntityListeners.set( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - subscriberElement.listenTo(), - subscriberElement, - ); - subscriber.splice(subscriber.indexOf(subscriberElement), 1); - } - } - } - - public enableEntityListeners( - entityType: object | object[] | string | string[], - ): void { - const entityTypesToCheck = Array.isArray(entityType) - ? entityType - : [entityType]; - const subscriber = this.manager.connection.subscribers; - - for (const entityTypesToCheckElement of entityTypesToCheck) { - const disabledEntity = AbstractRepository.disabledEntityListeners.get( - entityTypesToCheckElement, - ); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - subscriber.push(disabledEntity); - AbstractRepository.disabledEntityListeners.delete( - entityTypesToCheckElement, - ); - } - } } diff --git a/src/database/repository/habitat-resource.repository.ts b/src/database/repository/habitat-resource.repository.ts index 505df99..8168485 100644 --- a/src/database/repository/habitat-resource.repository.ts +++ b/src/database/repository/habitat-resource.repository.ts @@ -1,9 +1,10 @@ import {Injectable} from '@nestjs/common'; -import {DataSource, In, UpdateResult} from 'typeorm'; +import {DataSource, In} from 'typeorm'; import {BuildingProductionRateModel} from '@warp-core/database/model/building-production-rate.model'; import {BuildingModel} from '@warp-core/database/model/building.model'; import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; +import {QueueElementCostModel} from '@warp-core/database/model/queue-element-cost.model'; import {AbstractRepository} from '@warp-core/database/repository/abstract.repository'; @Injectable() @@ -46,6 +47,15 @@ export class HabitatResourceRepository extends AbstractRepository { + const requiredResourcesIds = queueCost.map(cost => cost.resource.id); + + return this.getHabitatResourcesByIds(requiredResourcesIds, habitatId); + } + public getHabitatResourcesByIds( resourcesIds: string[], habitatId: number, @@ -55,17 +65,4 @@ export class HabitatResourceRepository extends AbstractRepository { - return this.createQueryBuilder() - .update(HabitatResourceModel) - .set({lastCalculationTime: lastCalculationTime}) - .where('resourceId IN (:...resourceIds)', {resourceIds: resourceIds}) - .andWhere('habitatId = :habitatId', {habitatId: habitatId}) - .execute(); - } } diff --git a/src/user/queue/building-queue/building-queue-handler.service.ts b/src/user/queue/building-queue/building-queue-handler.service.ts index 79f2dc4..3a8f937 100644 --- a/src/user/queue/building-queue/building-queue-handler.service.ts +++ b/src/user/queue/building-queue/building-queue-handler.service.ts @@ -3,7 +3,6 @@ import {Injectable, Logger} from '@nestjs/common'; import {AuthorizedHabitatModel} from '@warp-core/auth'; import {BuildingQueueElementModel} from '@warp-core/database/model/building-queue-element.model'; import {BuildingZoneModel} from '@warp-core/database/model/building-zone.model'; -import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; import {BuildingQueueRepository} from '@warp-core/database/repository/building-queue.repository'; import {BuildingZoneRepository} from '@warp-core/database/repository/building-zone.repository'; import {BuildingQueueProcessingEmitter} from '@warp-core/user/queue/building-queue/exchange/emit/building-queue-processing.emitter'; @@ -86,11 +85,6 @@ export class BuildingQueueHandlerService { `Queue element processed/consumed for building zone with id ${queueElement.buildingZoneId}`, ); - this.buildingQueueRepository.disableEntityListeners([ - BuildingZoneModel, - HabitatResourceModel, - ]); - await this.buildingQueueEmitter.beforeProcessing({queueElement}); await this.buildingZoneRepository.update(buildingZoneToProcess.id, { @@ -102,10 +96,5 @@ export class BuildingQueueHandlerService { }); await this.buildingQueueEmitter.afterProcessing({queueElement}); - - this.buildingQueueRepository.enableEntityListeners([ - BuildingZoneModel, - HabitatResourceModel, - ]); } } diff --git a/src/user/queue/building-queue/building-queue.module.ts b/src/user/queue/building-queue/building-queue.module.ts index 1e864bc..d2faabd 100644 --- a/src/user/queue/building-queue/building-queue.module.ts +++ b/src/user/queue/building-queue/building-queue.module.ts @@ -1,4 +1,4 @@ -import {Module} from '@nestjs/common'; +import {MiddlewareConsumer, Module, NestModule} from '@nestjs/common'; import {AuthModule} from '@warp-core/auth'; import {CoreConfigModule} from '@warp-core/core/config/core-config.module'; @@ -11,7 +11,6 @@ import {SimpleCalculationService} from '@warp-core/user/queue/building-queue/add import {PrepareSingleBuildingQueueElementService} from '@warp-core/user/queue/building-queue/add/prepare-single-building-queue-element.service'; import {BuildingQueueHandlerService} from '@warp-core/user/queue/building-queue/building-queue-handler.service'; import {BuildingQueueResolver} from '@warp-core/user/queue/building-queue/building-queue.resolver'; -import {BuildingZoneUpdateByQueueSubscriber} from '@warp-core/user/queue/building-queue/entity-subscriber/building-zone-update-by-queue.subscriber'; import {BuildingQueueAddEmitter} from '@warp-core/user/queue/building-queue/exchange/emit/building-queue-add.emitter'; import {BuildingQueueProcessingEmitter} from '@warp-core/user/queue/building-queue/exchange/emit/building-queue-processing.emitter'; import {AddToQueueValidator} from '@warp-core/user/queue/building-queue/input/validator/add-to-queue.validator'; @@ -20,6 +19,7 @@ import {DraftQueueElementValidator} from '@warp-core/user/queue/building-queue/i import {EndLevelValidator} from '@warp-core/user/queue/building-queue/input/validator/end-level.validator'; import {MaxQueueCountValidator} from '@warp-core/user/queue/building-queue/input/validator/max-queue-count.validator'; import {ValidateSingleQueueElementService} from '@warp-core/user/queue/building-queue/input/validator/validate-single-queue-element.service'; +import {QueueConsumerMiddleware} from '@warp-core/user/queue/building-queue/queue-consumer.middleware'; @Module({ providers: [ @@ -31,7 +31,6 @@ import {ValidateSingleQueueElementService} from '@warp-core/user/queue/building- BuildingQueueHandlerService, PrepareSingleBuildingQueueElementService, BuildingQueueResolver, - BuildingZoneUpdateByQueueSubscriber, AddToQueueValidator, DraftQueueElementValidator, EndLevelValidator, @@ -40,6 +39,7 @@ import {ValidateSingleQueueElementService} from '@warp-core/user/queue/building- BuildingQueueAddEmitter, BuildingQueueProcessingEmitter, ValidateSingleQueueElementService, + QueueConsumerMiddleware, { provide: 'QUEUE_ADD_CALCULATION', useClass: SimpleCalculationService, @@ -47,4 +47,8 @@ import {ValidateSingleQueueElementService} from '@warp-core/user/queue/building- ], imports: [DatabaseModule, CoreConfigModule, AuthModule], }) -export class BuildingQueueModule {} +export class BuildingQueueModule implements NestModule { + public configure(consumer: MiddlewareConsumer): void { + consumer.apply(QueueConsumerMiddleware).forRoutes('graphql'); + } +} diff --git a/src/user/queue/building-queue/entity-subscriber/building-zone-update-by-queue.subscriber.ts b/src/user/queue/building-queue/entity-subscriber/building-zone-update-by-queue.subscriber.ts deleted file mode 100644 index a685267..0000000 --- a/src/user/queue/building-queue/entity-subscriber/building-zone-update-by-queue.subscriber.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {Injectable, Logger} from '@nestjs/common'; -import {DataSource, EntitySubscriberInterface, EventSubscriber} from 'typeorm'; - -import {AuthorizedHabitatModel} from '@warp-core/auth'; -import {BuildingZoneModel} from '@warp-core/database/model/building-zone.model'; -import {BuildingQueueHandlerService} from '@warp-core/user/queue/building-queue/building-queue-handler.service'; - -@Injectable() -@EventSubscriber() -export class BuildingZoneUpdateByQueueSubscriber - implements EntitySubscriberInterface -{ - private readonly logger = new Logger( - BuildingZoneUpdateByQueueSubscriber.name, - ); - - constructor( - private readonly dataSource: DataSource, - private readonly buildingQueueHandler: BuildingQueueHandlerService, - private readonly habitatModel: AuthorizedHabitatModel, - ) { - dataSource.subscribers.push(this); - } - - public listenTo(): typeof BuildingZoneModel { - return BuildingZoneModel; - } - - public async afterLoad(entity: BuildingZoneModel): Promise { - if (!this.habitatModel.id) { - return; - } - - this.logger.debug(`Resolving queue for building zone with id ${entity.id}`); - await this.buildingQueueHandler.resolveQueueForSingleBuildingZone(entity); - this.logger.debug(`Queue for building zone with id ${entity.id} resolved`); - } -} diff --git a/src/user/queue/building-queue/input/validator/max-queue-count.validator.spec.ts b/src/user/queue/building-queue/input/validator/max-queue-count.validator.spec.ts index ee85436..903ddc4 100644 --- a/src/user/queue/building-queue/input/validator/max-queue-count.validator.spec.ts +++ b/src/user/queue/building-queue/input/validator/max-queue-count.validator.spec.ts @@ -1,7 +1,6 @@ import {Test, TestingModule} from '@nestjs/testing'; import {when} from 'jest-when'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {RuntimeConfig} from '@warp-core/core/config/runtime.config'; import {BuildingZoneModel} from '@warp-core/database/model/building-zone.model'; import {BuildingModel} from '@warp-core/database/model/building.model'; @@ -19,7 +18,6 @@ jest.mock('@nestjs/config'); describe('max queue elements count validator', () => { let buildingQueueRepository: jest.Mocked; - let habitatMock: jest.Mocked; let runtimeConfig: jest.Mocked; let maxQueueCountValidator: MaxQueueCountValidator; @@ -28,30 +26,29 @@ describe('max queue elements count validator', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - BuildingQueueRepository, - AuthorizedHabitatModel, MaxQueueCountValidator, + BuildingQueueRepository, coreConfigMock, ], }).compile(); buildingQueueRepository = module.get(BuildingQueueRepository); runtimeConfig = module.get(RuntimeConfig); - habitatMock = module.get(AuthorizedHabitatModel); maxQueueCountValidator = module.get(MaxQueueCountValidator); }); describe('validate', () => { it('should add error when queue count equals max elements in queue from config', async () => { - habitatMock.id = 5; const maxQueueElements = 10; const queueValidationInput: QueueInputValidation = { addToQueueInput: {} as AddToQueueInput, building: {} as BuildingModel, - buildingZone: {} as BuildingZoneModel, + buildingZone: { + habitatId: 5, + } as BuildingZoneModel, validationError: new QueueValidationError(), }; when(buildingQueueRepository.countActiveBuildingQueueElementsForHabitat) - .calledWith(habitatMock.id) + .calledWith(5) .mockResolvedValue(maxQueueElements); runtimeConfig.habitat.buildingQueue.maxElementsInQueue = maxQueueElements; @@ -65,17 +62,18 @@ describe('max queue elements count validator', () => { }); it('should pass validation when queue elements count does not reach max queue elements from config', async () => { - habitatMock.id = 5; const maxQueueElements = 10; const queueValidationInput: QueueInputValidation = { addToQueueInput: {} as AddToQueueInput, building: {} as BuildingModel, - buildingZone: {} as BuildingZoneModel, + buildingZone: { + habitatId: 5, + } as BuildingZoneModel, validationError: new QueueValidationError(), }; when(buildingQueueRepository.countActiveBuildingQueueElementsForHabitat) - .calledWith(habitatMock.id) + .calledWith(5) .mockResolvedValue(3); runtimeConfig.habitat.buildingQueue.maxElementsInQueue = maxQueueElements; diff --git a/src/user/queue/building-queue/input/validator/max-queue-count.validator.ts b/src/user/queue/building-queue/input/validator/max-queue-count.validator.ts index 279eb04..d97fd56 100644 --- a/src/user/queue/building-queue/input/validator/max-queue-count.validator.ts +++ b/src/user/queue/building-queue/input/validator/max-queue-count.validator.ts @@ -1,6 +1,5 @@ import {Injectable} from '@nestjs/common'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {RuntimeConfig} from '@warp-core/core/config/runtime.config'; import {BuildingQueueRepository} from '@warp-core/database/repository/building-queue.repository'; import {QueueItemValidatorInterface} from '@warp-core/user/queue/building-queue/input/validator/queue-item-validator.interface'; @@ -11,15 +10,15 @@ export class MaxQueueCountValidator implements QueueItemValidatorInterface { constructor( private readonly buildingQueueRepository: BuildingQueueRepository, private readonly runtimeConfig: RuntimeConfig, - private readonly habitatModel: AuthorizedHabitatModel, ) {} public async validate({ validationError, + buildingZone, }: QueueInputValidation): Promise { const queueCounter = await this.buildingQueueRepository.countActiveBuildingQueueElementsForHabitat( - this.habitatModel.id, + buildingZone.habitatId, ); const maxElementsInQueue = this.runtimeConfig.habitat.buildingQueue.maxElementsInQueue; diff --git a/src/user/queue/building-queue/queue-consumer.middleware.ts b/src/user/queue/building-queue/queue-consumer.middleware.ts new file mode 100644 index 0000000..0b9c3f0 --- /dev/null +++ b/src/user/queue/building-queue/queue-consumer.middleware.ts @@ -0,0 +1,25 @@ +import {Injectable, NestMiddleware} from '@nestjs/common'; +import {NextFunction} from 'connect'; + +import {AuthorizedHabitatModel} from '@warp-core/auth'; +import {BuildingQueueHandlerService} from '@warp-core/user/queue/building-queue/building-queue-handler.service'; + +@Injectable() +export class QueueConsumerMiddleware implements NestMiddleware { + public constructor( + private readonly habitatModel: AuthorizedHabitatModel, + private readonly buildingQueueHandlerService: BuildingQueueHandlerService, + ) {} + + public async use( + req: Request, + res: Response, + next: NextFunction, + ): Promise { + if (this.habitatModel.id) { + await this.buildingQueueHandlerService.resolveQueue(); + } + + next(); + } +} diff --git a/src/user/resources/exchange/subscriber/queue-processing.subscriber.ts b/src/user/resources/exchange/subscriber/queue-processing.subscriber.ts index 7f64d47..7882722 100644 --- a/src/user/resources/exchange/subscriber/queue-processing.subscriber.ts +++ b/src/user/resources/exchange/subscriber/queue-processing.subscriber.ts @@ -5,32 +5,38 @@ import { BuildingQueueNames, BuildingQueueProcessing, } from '@warp-core/user/queue/building-queue'; -import {ResourceCalculatorService} from '@warp-core/user/resources/service/calculate/resource-calculator.service'; -import {HabitatHasNewResourceProducerService} from '@warp-core/user/resources/service/habitat-has-new-resource-producer.service'; import {QueueResourceExtractorService} from '@warp-core/user/resources/service/queue-resource-extractor.service'; +import {RecalculateResourcesOnQueueUpdate} from '@warp-core/user/resources/service/recalculate-resources-on-queue-update.service'; +import {ValidateQueueResourcesService} from '@warp-core/user/resources/service/validate-queue-resources.service'; @Injectable() export class QueueProcessingSubscriber { constructor( - private readonly resourceCalculatorService: ResourceCalculatorService, - private readonly newResourcesProducer: HabitatHasNewResourceProducerService, + private readonly validateQueueResourcesService: ValidateQueueResourcesService, private readonly queueExtractor: QueueResourceExtractorService, + private readonly habitatHasNewResourceProducerService: RecalculateResourcesOnQueueUpdate, ) {} @InternalExchangeEmitListener(BuildingQueueNames.BeforeAddingElement) - public async addResourcesOnQueueUpdate( + public async validateResourcesToBeConsumedByQueue( input: BuildingQueueProcessing, ): Promise { - await this.resourceCalculatorService.addResourcesOnQueueUpdate(input); - await this.newResourcesProducer.updateLastCalculationDateOnHabitatResource( - input, - ); + await this.validateQueueResourcesService.validate(input); } @InternalExchangeEmitListener(BuildingQueueNames.AfterAddingElement) - public setLastCalculationTimeForNewResources( + public consumeResourcesOnQueue( input: BuildingQueueProcessing, ): Promise { return this.queueExtractor.useResourcesOnQueueUpdate(input); } + + @InternalExchangeEmitListener(BuildingQueueNames.BeforeProcessingElement) + public setLastCalculationTimeAfterQueueProcessing( + input: BuildingQueueProcessing, + ): Promise { + return this.habitatHasNewResourceProducerService.updateLastCalculationDateOnHabitatResource( + input, + ); + } } diff --git a/src/user/resources/resources.module.ts b/src/user/resources/resources.module.ts index 61c1a8c..7cdd102 100644 --- a/src/user/resources/resources.module.ts +++ b/src/user/resources/resources.module.ts @@ -19,15 +19,15 @@ import {BaseResourceStorageService} from '@warp-core/user/resources/service/calc import {DisabledWarehouseStorageService} from '@warp-core/user/resources/service/calculate/warehouse-storage/disabled-warehouse-storage.service'; import {WarehouseStorageCalculationMechanic} from '@warp-core/user/resources/service/calculate/warehouse-storage/warehouse-storage-calculation-mechanic.interface'; import {CreateResourcesPerHabitatService} from '@warp-core/user/resources/service/create-resources-per-habitat.service'; -import {HabitatHasNewResourceProducerService} from '@warp-core/user/resources/service/habitat-has-new-resource-producer.service'; import {QueueResourceExtractorService} from '@warp-core/user/resources/service/queue-resource-extractor.service'; +import {RecalculateResourcesOnQueueUpdate} from '@warp-core/user/resources/service/recalculate-resources-on-queue-update.service'; import {ResourcesService} from '@warp-core/user/resources/service/resources.service'; +import {ValidateQueueResourcesService} from '@warp-core/user/resources/service/validate-queue-resources.service'; @Module({ providers: [ CreateResourcesPerHabitatService, HabitatResourceRecalculateSubscriber, - HabitatHasNewResourceProducerService, ResourcesService, ResourceCalculatorService, QueueResourceExtractorService, @@ -40,6 +40,8 @@ import {ResourcesService} from '@warp-core/user/resources/service/resources.serv NewHabitatSubscriber, QueueProcessingSubscriber, NoDistanceSimpleMultiplyResourceCalculationMechanicService, + ValidateQueueResourcesService, + RecalculateResourcesOnQueueUpdate, RegisterMechanic.forFeature( CalculationMechanic, 'runtime.mechanics.resources.calculation', diff --git a/src/user/resources/service/calculate/resource-calculator.service.spec.ts b/src/user/resources/service/calculate/resource-calculator.service.spec.ts index d82346a..4fadff9 100644 --- a/src/user/resources/service/calculate/resource-calculator.service.spec.ts +++ b/src/user/resources/service/calculate/resource-calculator.service.spec.ts @@ -1,14 +1,7 @@ import {Test, TestingModule} from '@nestjs/testing'; import {when} from 'jest-when'; -import {DateTime} from 'luxon'; -import {BuildingQueueElementModel} from '@warp-core/database/model/building-queue-element.model'; -import {BuildingZoneModel} from '@warp-core/database/model/building-zone.model'; -import {BuildingModel} from '@warp-core/database/model/building.model'; import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; -import {HabitatModel} from '@warp-core/database/model/habitat.model'; -import {ResourceModel} from '@warp-core/database/model/resource.model'; -import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; import {CalculationMechanic} from '@warp-core/user/resources/service/calculate/resource-calculation/calculation-mechanic.interface'; import {ResourceCalculatorService} from '@warp-core/user/resources/service/calculate/resource-calculator.service'; import {WarehouseStorageCalculationMechanic} from '@warp-core/user/resources/service/calculate/warehouse-storage/warehouse-storage-calculation-mechanic.interface'; @@ -20,7 +13,6 @@ jest.mock( describe('Resources calculator service test', () => { let resourcesCalculator: ResourceCalculatorService; - let habitatResourceRepository: jest.Mocked; let warehouseStorage: jest.Mocked; let calculateMechanic: jest.Mocked; @@ -29,7 +21,6 @@ describe('Resources calculator service test', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ResourceCalculatorService, - HabitatResourceRepository, { provide: WarehouseStorageCalculationMechanic, useValue: { @@ -49,7 +40,6 @@ describe('Resources calculator service test', () => { resourcesCalculator = module.get( ResourceCalculatorService, ); - habitatResourceRepository = module.get(HabitatResourceRepository); warehouseStorage = module.get(WarehouseStorageCalculationMechanic); calculateMechanic = module.get(CalculationMechanic); @@ -107,61 +97,4 @@ describe('Resources calculator service test', () => { expect(habitatResource.currentAmount).toEqual(100); }); }); - - describe('addResourcesOnQueueUpdate', () => { - it('should update resource on queue update for building zone with single resource', async () => { - const habitatModel = {id: 5} as HabitatModel; - const now = new Date(); - const building = {id: 'mine'} as BuildingModel; - const buildingZone = { - id: 1, - habitatId: habitatModel.id, - habitat: habitatModel, - } as BuildingZoneModel; - const buildingQueueElement = { - building, - buildingZone, - buildingZoneId: buildingZone.id, - startLevel: 1, - endTime: now, - } as BuildingQueueElementModel; - const resource = {id: 'wood'} as ResourceModel; - - const habitatResource = { - id: '1', - currentAmount: 0, - resourceId: 'wood', - habitatId: habitatModel.id, - habitat: habitatModel, - lastCalculationTime: DateTime.now().minus(10000).toJSDate(), - resource, - } as HabitatResourceModel; - - when(habitatResourceRepository.getHabitatResourceByBuildingAndLevel) - .expectCalledWith( - building, - buildingQueueElement.startLevel, - buildingZone.habitatId, - ) - .mockResolvedValue([habitatResource]); - - when(calculateMechanic.calculateCurrentResourceValue) - .calledWith(habitatResource, expect.any(Number)) - .mockResolvedValue(20); - when(warehouseStorage.calculateStorage) - .calledWith(resource, habitatModel) - .mockResolvedValue(100); - - await resourcesCalculator.addResourcesOnQueueUpdate({ - queueElement: buildingQueueElement, - }); - - expect(habitatResource.currentAmount).toEqual(20); - expect(habitatResource.lastCalculationTime.getTime()).toEqual( - now.getTime(), - ); - - expect(habitatResourceRepository.save).toHaveBeenCalledTimes(1); - }); - }); }); diff --git a/src/user/resources/service/calculate/resource-calculator.service.ts b/src/user/resources/service/calculate/resource-calculator.service.ts index 615e9e2..95d3f68 100644 --- a/src/user/resources/service/calculate/resource-calculator.service.ts +++ b/src/user/resources/service/calculate/resource-calculator.service.ts @@ -2,8 +2,6 @@ import {Injectable, Logger} from '@nestjs/common'; import {DateTime} from 'luxon'; import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; -import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; -import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; import {CalculationMechanic} from '@warp-core/user/resources/service/calculate/resource-calculation/calculation-mechanic.interface'; import {WarehouseStorageCalculationMechanic} from '@warp-core/user/resources/service/calculate/warehouse-storage/warehouse-storage-calculation-mechanic.interface'; @@ -12,7 +10,6 @@ export class ResourceCalculatorService { private readonly logger = new Logger(ResourceCalculatorService.name); constructor( - private readonly habitatResourceRepository: HabitatResourceRepository, private readonly calculateResourceStorage: WarehouseStorageCalculationMechanic, private readonly calculationMechanic: CalculationMechanic, ) {} @@ -56,33 +53,6 @@ export class ResourceCalculatorService { ); } - public async addResourcesOnQueueUpdate({ - queueElement: buildingQueueElement, - }: BuildingQueueProcessing): Promise { - const {habitatId} = await buildingQueueElement.buildingZone; - - this.logger.debug( - `Calculating resource on queue update for building zone ${buildingQueueElement.buildingZoneId}`, - ); - - const habitatResources = - await this.habitatResourceRepository.getHabitatResourceByBuildingAndLevel( - await buildingQueueElement.building!, - buildingQueueElement.startLevel, - habitatId, - ); - - for (const singleHabitatResource of habitatResources) { - await this.calculateSingleResource( - singleHabitatResource, - buildingQueueElement.endTime, - ); - singleHabitatResource.lastCalculationTime = buildingQueueElement.endTime; - } - - await this.habitatResourceRepository.save(habitatResources); - } - private calculateTimeToProcessResources( habitatResource: HabitatResourceModel, calculationEndTime: Date = new Date(), diff --git a/src/user/resources/service/queue-resource-extractor.service.spec.ts b/src/user/resources/service/queue-resource-extractor.service.spec.ts index 839c290..285482e 100644 --- a/src/user/resources/service/queue-resource-extractor.service.spec.ts +++ b/src/user/resources/service/queue-resource-extractor.service.spec.ts @@ -1,14 +1,11 @@ import {Test, TestingModule} from '@nestjs/testing'; import {when} from 'jest-when'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; import {QueueElementCostModel} from '@warp-core/database/model/queue-element-cost.model'; import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; import {prepareRepositoryMock} from '@warp-core/test/database/repository/prepare-repository-mock'; import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; -import {InsufficientResourceType} from '@warp-core/user/resources/exception/insufficient-resource.type'; -import {InsufficientResourcesException} from '@warp-core/user/resources/exception/Insufficient-resources.exception'; import {QueueResourceExtractorService} from '@warp-core/user/resources/service/queue-resource-extractor.service'; jest.mock('@warp-core/database/repository/habitat-resource.repository'); @@ -16,7 +13,6 @@ jest.mock('@warp-core/database/repository/habitat-resource.repository'); describe('Resource extraction service', () => { let resourceExtractorService: QueueResourceExtractorService; let habitatResourceRepository: jest.Mocked; - let authorizedHabitatModel: jest.Mocked; beforeAll(() => { prepareRepositoryMock(HabitatResourceRepository); @@ -25,23 +21,17 @@ describe('Resource extraction service', () => { beforeEach(async () => { jest.clearAllMocks(); const module: TestingModule = await Test.createTestingModule({ - providers: [ - QueueResourceExtractorService, - HabitatResourceRepository, - AuthorizedHabitatModel, - ], + providers: [QueueResourceExtractorService, HabitatResourceRepository], }).compile(); resourceExtractorService = module.get( QueueResourceExtractorService, ); habitatResourceRepository = module.get(HabitatResourceRepository); - authorizedHabitatModel = module.get(AuthorizedHabitatModel); }); describe('useResourcesOnQueueUpdate', () => { - it('should throw exception in edge case when resources from habitat has less quantity than required resources', async () => { - authorizedHabitatModel.id = 1; + it('should extract resources when user have enough resources to be used', async () => { const costs = [ { resource: { @@ -57,222 +47,14 @@ describe('Resource extraction service', () => { }, ] as QueueElementCostModel[]; - const habitatResources = [ - { - resourceId: 'wood', - currentAmount: 0, - }, - ] as HabitatResourceModel[]; - - when(habitatResourceRepository.getHabitatResourcesByIds) - .expectCalledWith( - expect.arrayContaining(['wood', 'stone']), - authorizedHabitatModel.id, - ) - .mockResolvedValue(habitatResources); - - await expect( - resourceExtractorService.useResourcesOnQueueUpdate({ - queueElement: {costs: costs}, - } as BuildingQueueProcessing), - ).rejects.toThrow( - 'Requested resources from queue does not equal resources from habitat', - ); - }); - - const insufficientResources = [ - { - name: 'there is single resource that is not enough', - queueCosts: [ - { - resource: { - id: 'wood', - name: 'Wood', - }, - cost: 10, - }, - ], - habitatResources: [ - { - resourceId: 'wood', - currentAmount: 0, - }, - ], - exceptionCalculationResults: [ - { - resourceId: 'wood', - difference: 10, - }, - ], - }, - { - name: 'there are multiple resources that are not enough', - queueCosts: [ - { - resource: { - id: 'wood', - name: 'Wood', - }, - cost: 10, - }, - { - resource: { - id: 'stone', - name: 'Stone', - }, - cost: 15, - }, - ], - habitatResources: [ - { - resourceId: 'wood', - currentAmount: 0, - }, - { - resourceId: 'stone', - currentAmount: 10, - }, - ], - exceptionCalculationResults: [ - { - resourceId: 'wood', - difference: 10, - }, - { - resourceId: 'stone', - difference: 5, - }, - ], - }, - { - name: 'there are multiple resources and one is not enough', - queueCosts: [ - { - resource: { - id: 'wood', - name: 'Wood', - }, - cost: 10, - }, - { - resource: { - id: 'stone', - name: 'Stone', - }, - cost: 15, - }, - ], - habitatResources: [ - { - resourceId: 'wood', - currentAmount: 0, - }, - { - resourceId: 'stone', - currentAmount: 15, - }, - ], - exceptionCalculationResults: [ - { - resourceId: 'wood', - difference: 10, - }, - ], - }, - ]; - - describe.each(insufficientResources)( - 'Insufficient resources', - singleCase => { - it(`should throw exception when ${singleCase.name}`, async () => { - authorizedHabitatModel.id = 1; - const costs = singleCase.queueCosts as QueueElementCostModel[]; - - const habitatResources = - singleCase.habitatResources as HabitatResourceModel[]; - - when(habitatResourceRepository.getHabitatResourcesByIds) - .expectCalledWith( - expect.arrayContaining( - singleCase.exceptionCalculationResults.map( - testCase => testCase.resourceId, - ), - ), - authorizedHabitatModel.id, - ) - .mockResolvedValue(habitatResources); - - try { - await resourceExtractorService.useResourcesOnQueueUpdate({ - queueElement: {costs: costs}, - } as BuildingQueueProcessing); - } catch (e) { - expect(e).toBeInstanceOf(InsufficientResourcesException); - expect(e.insufficientResources).toHaveLength( - singleCase.exceptionCalculationResults.length, - ); - const insufficientResources: InsufficientResourceType[] = - e.insufficientResources; - - for (const calculationResult of singleCase.exceptionCalculationResults) { - const resourceFromException = ( - insufficientResources.find( - singleResourceFromException => - calculationResult.resourceId === - singleResourceFromException.resourceId, - ) - ); - const queueCost = singleCase.queueCosts.find( - singleElement => - singleElement.resource.id === calculationResult.resourceId, - ); - - const habitatResource = singleCase.habitatResources.find( - singleResource => - singleResource.resourceId === calculationResult.resourceId, - ); - - if ( - typeof queueCost === 'undefined' || - typeof habitatResource === 'undefined' - ) { - continue; - } - - expect(resourceFromException.resourceId).toEqual( - queueCost.resource.id, - ); - expect(resourceFromException.requiredResources).toEqual( - queueCost.cost, - ); - expect(resourceFromException.currentResources).toEqual( - habitatResource.currentAmount, - ); - expect(resourceFromException.difference).toEqual( - calculationResult.difference, - ); - } - } - }); - }, - ); - - it('should extract resources when user have enough resources to be used', async () => { - authorizedHabitatModel.id = 1; - const costs = [ - { - resource: { - id: 'wood', - }, - cost: 10, - }, - { - resource: { - id: 'stone', + const queueElement = { + queueElement: { + costs, + buildingZone: { + habitatId: 1, }, - cost: 10, }, - ] as QueueElementCostModel[]; + } as BuildingQueueProcessing; const habitatResources = [ { @@ -285,16 +67,11 @@ describe('Resource extraction service', () => { }, ] as HabitatResourceModel[]; - when(habitatResourceRepository.getHabitatResourcesByIds) - .expectCalledWith( - expect.arrayContaining(['wood', 'stone']), - authorizedHabitatModel.id, - ) + when(habitatResourceRepository.getHabitatResourcesByQueueCostItems) + .expectCalledWith(costs, 1) .mockResolvedValue(habitatResources); - await resourceExtractorService.useResourcesOnQueueUpdate({ - queueElement: {costs: costs}, - } as BuildingQueueProcessing); + await resourceExtractorService.useResourcesOnQueueUpdate(queueElement); expect(habitatResources[0].currentAmount).toEqual(0); expect(habitatResources[1].currentAmount).toEqual(0); diff --git a/src/user/resources/service/queue-resource-extractor.service.ts b/src/user/resources/service/queue-resource-extractor.service.ts index ec73574..7e74202 100644 --- a/src/user/resources/service/queue-resource-extractor.service.ts +++ b/src/user/resources/service/queue-resource-extractor.service.ts @@ -1,17 +1,13 @@ -import {BadRequestException, Injectable} from '@nestjs/common'; +import {Injectable} from '@nestjs/common'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; import {QueueElementCostModel} from '@warp-core/database/model/queue-element-cost.model'; import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; -import {InsufficientResourceType} from '@warp-core/user/resources/exception/insufficient-resource.type'; -import {InsufficientResourcesException} from '@warp-core/user/resources/exception/Insufficient-resources.exception'; @Injectable() export class QueueResourceExtractorService { constructor( - private readonly habitatModel: AuthorizedHabitatModel, private readonly habitatResourceRepository: HabitatResourceRepository, ) {} @@ -19,68 +15,14 @@ export class QueueResourceExtractorService { queueProcessingEvent: BuildingQueueProcessing, ): Promise { const queueElement = queueProcessingEvent.queueElement; - const requiredResources = await this.getRequiredResourcesFromHabitat( - queueElement.costs, - ); - const errors = this.validateResources( - queueElement.costs, - requiredResources, - ); - - if (errors.length > 0) { - throw new InsufficientResourcesException(errors); - } - - await this.extractResources( - queueElement.costs, - requiredResources, - queueElement.endTime, - ); - } - - private async getRequiredResourcesFromHabitat( - queueCost: QueueElementCostModel[], - ): Promise { - const requiredResourcesIds = queueCost.map(cost => cost.resource.id); - - const resourcesFromHabitat = - await this.habitatResourceRepository.getHabitatResourcesByIds( - requiredResourcesIds, - this.habitatModel.id, + const requiredResources = + await this.habitatResourceRepository.getHabitatResourcesByQueueCostItems( + queueElement.costs, + (await queueElement.buildingZone).habitatId, ); + const now = new Date(); - if (resourcesFromHabitat.length !== requiredResourcesIds.length) { - throw new BadRequestException( - 'Requested resources from queue does not equal resources from habitat', - ); - } - - return resourcesFromHabitat; - } - - private validateResources( - queueCost: QueueElementCostModel[], - requiredResources: HabitatResourceModel[], - ): InsufficientResourceType[] { - const errors: InsufficientResourceType[] = []; - - for (const singleCost of queueCost) { - const habitatResourceModel = requiredResources.find( - singleResource => singleResource.resourceId === singleCost.resource.id, - ) as HabitatResourceModel; - - if (habitatResourceModel.currentAmount < singleCost.cost) { - const difference = singleCost.cost - habitatResourceModel.currentAmount; - errors.push({ - resourceId: singleCost.resource.id, - resourceName: singleCost.resource.name, - requiredResources: singleCost.cost, - currentResources: habitatResourceModel.currentAmount, - difference: difference, - }); - } - } - return errors; + await this.extractResources(queueElement.costs, requiredResources, now); } private async extractResources( diff --git a/src/user/resources/service/habitat-has-new-resource-producer.service.spec.ts b/src/user/resources/service/recalculate-resources-on-queue-update.service.spec.ts similarity index 63% rename from src/user/resources/service/habitat-has-new-resource-producer.service.spec.ts rename to src/user/resources/service/recalculate-resources-on-queue-update.service.spec.ts index ef7b589..d090faf 100644 --- a/src/user/resources/service/habitat-has-new-resource-producer.service.spec.ts +++ b/src/user/resources/service/recalculate-resources-on-queue-update.service.spec.ts @@ -1,22 +1,26 @@ import {Test, TestingModule} from '@nestjs/testing'; import {when} from 'jest-when'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {BuildingProductionRateModel} from '@warp-core/database/model/building-production-rate.model'; import {BuildingQueueElementModel} from '@warp-core/database/model/building-queue-element.model'; +import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; import {BuildingRepository} from '@warp-core/database/repository/building.repository'; import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; import {prepareRepositoryMock} from '@warp-core/test/database/repository/prepare-repository-mock'; -import {HabitatHasNewResourceProducerService} from '@warp-core/user/resources/service/habitat-has-new-resource-producer.service'; +import {ResourceCalculatorService} from '@warp-core/user/resources/service/calculate/resource-calculator.service'; +import {RecalculateResourcesOnQueueUpdate} from '@warp-core/user/resources/service/recalculate-resources-on-queue-update.service'; jest.mock('@warp-core/database/repository/building.repository'); jest.mock('@warp-core/database/repository/habitat-resource.repository'); +jest.mock( + '@warp-core/user/resources/service/calculate/resource-calculator.service', +); describe('Add last calculation date for new resource producers', () => { let buildingRepository: jest.Mocked; let habitatResourceRepository: jest.Mocked; - let authorizedHabitatModel: AuthorizedHabitatModel; - let habitatHasNewResourceProducerSubscriber: HabitatHasNewResourceProducerService; + let resourceCalculatorService: jest.Mocked; + let habitatHasNewResourceProducerSubscriber: RecalculateResourcesOnQueueUpdate; beforeAll(() => { prepareRepositoryMock(HabitatResourceRepository); @@ -29,25 +33,28 @@ describe('Add last calculation date for new resource producers', () => { providers: [ BuildingRepository, HabitatResourceRepository, - AuthorizedHabitatModel, - HabitatHasNewResourceProducerService, + RecalculateResourcesOnQueueUpdate, + ResourceCalculatorService, ], }).compile(); buildingRepository = module.get(BuildingRepository); habitatResourceRepository = module.get(HabitatResourceRepository); - authorizedHabitatModel = module.get(AuthorizedHabitatModel); + resourceCalculatorService = module.get(ResourceCalculatorService); habitatHasNewResourceProducerSubscriber = module.get( - HabitatHasNewResourceProducerService, + RecalculateResourcesOnQueueUpdate, ); }); describe('updateLastCalculationDateOnHabitatResource', () => { it('should do nothing when building does not produce anything ad selected level', async () => { - authorizedHabitatModel.id = 5; + const habitatId = 5; const queueElement = { endLevel: 5, buildingId: 'test', + buildingZone: { + habitatId, + }, } as BuildingQueueElementModel; const productionRateModels = [] as BuildingProductionRateModel[]; @@ -63,16 +70,22 @@ describe('Add last calculation date for new resource producers', () => { {queueElement: queueElement}, ); + expect(habitatResourceRepository.save).toHaveBeenCalledTimes(0); + expect( - habitatResourceRepository.updateLastCalculationDateForManyResources, + resourceCalculatorService.calculateSingleResource, ).toHaveBeenCalledTimes(0); }); it('should update habitat resource when building produce resources', async () => { - authorizedHabitatModel.id = 5; + const habitatId = 5; const queueElement = { endLevel: 5, buildingId: 'test', + endTime: new Date(), + buildingZone: { + habitatId, + }, } as BuildingQueueElementModel; const productionRateModels = [ @@ -84,6 +97,17 @@ describe('Add last calculation date for new resource producers', () => { }, ] as BuildingProductionRateModel[]; + const resources = [ + { + resourceId: 'wood', + lastCalculationTime: new Date(), + }, + { + resourceId: 'coal', + lastCalculationTime: new Date(), + }, + ] as HabitatResourceModel[]; + when(buildingRepository.getProductionRateForProvidedLevel) .expectCalledWith( queueElement.buildingId as string, @@ -91,13 +115,22 @@ describe('Add last calculation date for new resource producers', () => { ) .mockResolvedValue(productionRateModels); + when(habitatResourceRepository.getHabitatResourcesByIds) + .expectCalledWith( + productionRateModels.map(productionRate => productionRate.resourceId), + habitatId, + ) + .mockResolvedValue(resources); + await habitatHasNewResourceProducerSubscriber.updateLastCalculationDateOnHabitatResource( {queueElement: queueElement}, ); + expect(habitatResourceRepository.save).toHaveBeenCalledTimes(1); + expect( - habitatResourceRepository.updateLastCalculationDateForManyResources, - ).toHaveBeenCalledTimes(1); + resourceCalculatorService.calculateSingleResource, + ).toHaveBeenCalledTimes(2); }); }); }); diff --git a/src/user/resources/service/habitat-has-new-resource-producer.service.ts b/src/user/resources/service/recalculate-resources-on-queue-update.service.ts similarity index 65% rename from src/user/resources/service/habitat-has-new-resource-producer.service.ts rename to src/user/resources/service/recalculate-resources-on-queue-update.service.ts index f647217..091feb5 100644 --- a/src/user/resources/service/habitat-has-new-resource-producer.service.ts +++ b/src/user/resources/service/recalculate-resources-on-queue-update.service.ts @@ -1,22 +1,20 @@ import {Injectable, Logger} from '@nestjs/common'; -import {AuthorizedHabitatModel} from '@warp-core/auth'; import {BuildingProductionRateModel} from '@warp-core/database/model/building-production-rate.model'; import {BuildingQueueElementModel} from '@warp-core/database/model/building-queue-element.model'; import {BuildingRepository} from '@warp-core/database/repository/building.repository'; import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; +import {ResourceCalculatorService} from '@warp-core/user/resources/service/calculate/resource-calculator.service'; @Injectable() -export class HabitatHasNewResourceProducerService { - private readonly logger = new Logger( - HabitatHasNewResourceProducerService.name, - ); +export class RecalculateResourcesOnQueueUpdate { + private readonly logger = new Logger(RecalculateResourcesOnQueueUpdate.name); constructor( private readonly buildingRepository: BuildingRepository, private readonly habitatResourceRepository: HabitatResourceRepository, - private readonly habitatModel: AuthorizedHabitatModel, + private readonly resourceCalculatorService: ResourceCalculatorService, ) {} public async updateLastCalculationDateOnHabitatResource( @@ -36,13 +34,23 @@ export class HabitatHasNewResourceProducerService { this.logger.debug('Updating last calculation date if needed'); - await this.habitatResourceRepository.updateLastCalculationDateForManyResources( - buildingProduction.map( - singleBuildingProduction => singleBuildingProduction.resourceId, - ), - this.habitatModel.id, - queueElement.endTime, - ); + const resources = + await this.habitatResourceRepository.getHabitatResourcesByIds( + buildingProduction.map( + singleBuildingProduction => singleBuildingProduction.resourceId, + ), + (await queueElement.buildingZone).habitatId, + ); + + for (const resource of resources) { + await this.resourceCalculatorService.calculateSingleResource( + resource, + queueElement.endTime, + ); + resource.lastCalculationTime = queueElement.endTime; + } + + await this.habitatResourceRepository.save(resources); this.logger.debug('Last calculation time from habitat resources updated'); } diff --git a/src/user/resources/service/validate-queue-resources.service.spec.ts b/src/user/resources/service/validate-queue-resources.service.spec.ts new file mode 100644 index 0000000..b98674b --- /dev/null +++ b/src/user/resources/service/validate-queue-resources.service.spec.ts @@ -0,0 +1,255 @@ +import {Test, TestingModule} from '@nestjs/testing'; +import {when} from 'jest-when'; + +import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; +import {QueueElementCostModel} from '@warp-core/database/model/queue-element-cost.model'; +import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; +import {prepareRepositoryMock} from '@warp-core/test/database/repository/prepare-repository-mock'; +import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; +import {InsufficientResourceType} from '@warp-core/user/resources/exception/insufficient-resource.type'; +import {InsufficientResourcesException} from '@warp-core/user/resources/exception/Insufficient-resources.exception'; +import {ValidateQueueResourcesService} from '@warp-core/user/resources/service/validate-queue-resources.service'; + +jest.mock('@warp-core/database/repository/habitat-resource.repository'); + +describe('Validate queue resources', () => { + let queueResourceValidatorService: ValidateQueueResourcesService; + let habitatResourceRepository: jest.Mocked; + + beforeAll(() => { + prepareRepositoryMock(HabitatResourceRepository); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + const module: TestingModule = await Test.createTestingModule({ + providers: [ValidateQueueResourcesService, HabitatResourceRepository], + }).compile(); + + queueResourceValidatorService = module.get( + ValidateQueueResourcesService, + ); + habitatResourceRepository = module.get(HabitatResourceRepository); + }); + + describe('useResourcesOnQueueUpdate', () => { + it('should throw exception in edge case when resources from habitat has less quantity than required resources', async () => { + const costs = [ + { + resource: { + id: 'wood', + }, + cost: 10, + }, + { + resource: { + id: 'stone', + }, + cost: 10, + }, + ] as QueueElementCostModel[]; + + const queueElement = { + queueElement: { + costs, + buildingZone: { + habitatId: 1, + }, + }, + } as BuildingQueueProcessing; + + const habitatResources = [ + { + resourceId: 'wood', + currentAmount: 0, + }, + ] as HabitatResourceModel[]; + + when(habitatResourceRepository.getHabitatResourcesByQueueCostItems) + .expectCalledWith(costs, 1) + .mockResolvedValue(habitatResources); + + await expect( + queueResourceValidatorService.validate(queueElement), + ).rejects.toThrow( + 'Requested resources from queue does not equal resources from habitat', + ); + }); + + const insufficientResources = [ + { + name: 'there is single resource that is not enough', + queueCosts: [ + { + resource: { + id: 'wood', + name: 'Wood', + }, + cost: 10, + }, + ], + habitatResources: [ + { + resourceId: 'wood', + currentAmount: 0, + }, + ], + exceptionCalculationResults: [ + { + resourceId: 'wood', + difference: 10, + }, + ], + }, + { + name: 'there are multiple resources that are not enough', + queueCosts: [ + { + resource: { + id: 'wood', + name: 'Wood', + }, + cost: 10, + }, + { + resource: { + id: 'stone', + name: 'Stone', + }, + cost: 15, + }, + ], + habitatResources: [ + { + resourceId: 'wood', + currentAmount: 0, + }, + { + resourceId: 'stone', + currentAmount: 10, + }, + ], + exceptionCalculationResults: [ + { + resourceId: 'wood', + difference: 10, + }, + { + resourceId: 'stone', + difference: 5, + }, + ], + }, + { + name: 'there are multiple resources and one is not enough', + queueCosts: [ + { + resource: { + id: 'wood', + name: 'Wood', + }, + cost: 10, + }, + { + resource: { + id: 'stone', + name: 'Stone', + }, + cost: 15, + }, + ], + habitatResources: [ + { + resourceId: 'wood', + currentAmount: 0, + }, + { + resourceId: 'stone', + currentAmount: 15, + }, + ], + exceptionCalculationResults: [ + { + resourceId: 'wood', + difference: 10, + }, + ], + }, + ]; + + describe.each(insufficientResources)( + 'Insufficient resources', + singleCase => { + it(`should throw exception when ${singleCase.name}`, async () => { + const costs = singleCase.queueCosts as QueueElementCostModel[]; + + const queueElement = { + queueElement: { + costs, + buildingZone: { + habitatId: 1, + }, + }, + } as BuildingQueueProcessing; + + const habitatResources = + singleCase.habitatResources as HabitatResourceModel[]; + + when(habitatResourceRepository.getHabitatResourcesByQueueCostItems) + .expectCalledWith(costs, 1) + .mockResolvedValue(habitatResources); + + try { + await queueResourceValidatorService.validate(queueElement); + } catch (e) { + expect(e).toBeInstanceOf(InsufficientResourcesException); + expect(e.insufficientResources).toHaveLength( + singleCase.exceptionCalculationResults.length, + ); + const insufficientResources: InsufficientResourceType[] = + e.insufficientResources; + + for (const calculationResult of singleCase.exceptionCalculationResults) { + const resourceFromException = ( + insufficientResources.find( + singleResourceFromException => + calculationResult.resourceId === + singleResourceFromException.resourceId, + ) + ); + const queueCost = singleCase.queueCosts.find( + singleElement => + singleElement.resource.id === calculationResult.resourceId, + ); + + const habitatResource = singleCase.habitatResources.find( + singleResource => + singleResource.resourceId === calculationResult.resourceId, + ); + + if ( + typeof queueCost === 'undefined' || + typeof habitatResource === 'undefined' + ) { + continue; + } + + expect(resourceFromException.resourceId).toEqual( + queueCost.resource.id, + ); + expect(resourceFromException.requiredResources).toEqual( + queueCost.cost, + ); + expect(resourceFromException.currentResources).toEqual( + habitatResource.currentAmount, + ); + expect(resourceFromException.difference).toEqual( + calculationResult.difference, + ); + } + } + }); + }, + ); + }); +}); diff --git a/src/user/resources/service/validate-queue-resources.service.ts b/src/user/resources/service/validate-queue-resources.service.ts new file mode 100644 index 0000000..598bc4b --- /dev/null +++ b/src/user/resources/service/validate-queue-resources.service.ts @@ -0,0 +1,65 @@ +import {BadRequestException, Injectable} from '@nestjs/common'; + +import {HabitatResourceModel} from '@warp-core/database/model/habitat-resource.model'; +import {QueueElementCostModel} from '@warp-core/database/model/queue-element-cost.model'; +import {HabitatResourceRepository} from '@warp-core/database/repository/habitat-resource.repository'; +import {BuildingQueueProcessing} from '@warp-core/user/queue/building-queue'; +import {InsufficientResourceType} from '@warp-core/user/resources/exception/insufficient-resource.type'; +import {InsufficientResourcesException} from '@warp-core/user/resources/exception/Insufficient-resources.exception'; + +@Injectable() +export class ValidateQueueResourcesService { + constructor( + private readonly habitatResourceRepository: HabitatResourceRepository, + ) {} + + public async validate({ + queueElement, + }: BuildingQueueProcessing): Promise { + const requiredResources = + await this.habitatResourceRepository.getHabitatResourcesByQueueCostItems( + queueElement.costs, + (await queueElement.buildingZone).habitatId, + ); + + if (requiredResources.length !== queueElement.costs.length) { + throw new BadRequestException( + 'Requested resources from queue does not equal resources from habitat', + ); + } + + const errors = this.validateResources( + queueElement.costs, + requiredResources, + ); + + if (errors.length > 0) { + throw new InsufficientResourcesException(errors); + } + } + + private validateResources( + queueCost: QueueElementCostModel[], + requiredResources: HabitatResourceModel[], + ): InsufficientResourceType[] { + const errors: InsufficientResourceType[] = []; + + for (const singleCost of queueCost) { + const habitatResourceModel = requiredResources.find( + singleResource => singleResource.resourceId === singleCost.resource.id, + ) as HabitatResourceModel; + + if (habitatResourceModel.currentAmount < singleCost.cost) { + const difference = singleCost.cost - habitatResourceModel.currentAmount; + errors.push({ + resourceId: singleCost.resource.id, + resourceName: singleCost.resource.name, + requiredResources: singleCost.cost, + currentResources: habitatResourceModel.currentAmount, + difference: difference, + }); + } + } + return errors; + } +} diff --git a/test/e2e/building-queue/building-queue-new-building-building-resources-check.spec.ts b/test/e2e/building-queue/building-queue-new-building-building-resources-check.spec.ts new file mode 100644 index 0000000..a89d753 --- /dev/null +++ b/test/e2e/building-queue/building-queue-new-building-building-resources-check.spec.ts @@ -0,0 +1,155 @@ +import {HttpStatus, INestApplication} from '@nestjs/common'; +import {DateTime} from 'luxon'; +import supertest from 'supertest'; + +import {requestGraphQL} from '@warp-core/test/e2e/utils/graphql-request-test'; +import {GraphqlRequestTest} from '@warp-core/test/e2e/utils/graphql-request-test/graphql-request-test'; +import {createNestApplicationE2E} from '@warp-core/test/e2e/utils/setup-tests'; + +describe('Building queue - basic update actions', () => { + let app: INestApplication; + let requestTest: GraphqlRequestTest; + + const localBuildingZone = 3; + + const now = DateTime.now(); + + function addBuildingToQueueRequest(endLevel: number): supertest.Test { + return requestTest + .mutation({ + root: 'buildingQueue_add', + operationName: 'AddToQueue', + fields: { + fields: ['startLevel', 'endLevel', 'isConsumed'], + }, + variables: { + addToQueue: { + type: 'AddToQueueInput', + value: { + buildingId: 'lumber_mill', + localBuildingZoneId: localBuildingZone, + endLevel, + }, + }, + }, + }) + .send(); + } + + beforeEach(async () => { + app = await createNestApplicationE2E(); + requestTest = requestGraphQL(app.getHttpServer()); + + await requestTest.registerAndAuthenticate(2); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('Create new building - lumber mill', () => { + beforeEach(async () => { + await addBuildingToQueueRequest(1); + }); + + it('should remove resources after putting element on queue', async () => { + const response = await requestTest + .query({ + root: 'resource_getAll', + fields: { + fields: ['id', 'currentAmount'], + }, + }) + .send() + .expect(HttpStatus.OK); + + const resourcesFromResponse = response.body.data.resource_getAll; + + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'wood', + value: 20, + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'stone_granite', + value: 0, + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'coal', + value: 5, + }); + }); + + it('Resources should not change after adding first level of lumber mill (wait 5s after adding first item)', async () => { + jest.useFakeTimers().setSystemTime(now.plus({second: 5}).toJSDate()); + + const response = await requestTest + .query({ + root: 'resource_getAll', + fields: { + fields: ['id', 'currentAmount', 'productionRate'], + }, + }) + .send() + .expect(HttpStatus.OK); + + const resourcesFromResponse = response.body.data.resource_getAll; + + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'wood', + value: 20, + }); + expect(resourcesFromResponse).toHaveResourceWithCustomProperty({ + resourceId: 'wood', + value: 0, + property: 'productionRate', + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'stone_granite', + value: 0, + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'coal', + value: 5, + }); + }); + + describe('Wait 15s after adding first item', () => { + beforeEach(async () => { + jest.useFakeTimers().setSystemTime(now.plus({second: 15}).toJSDate()); + }); + + it('Resources should change after consuming building queue', async () => { + const response = await requestTest + .query({ + root: 'resource_getAll', + fields: { + fields: ['id', 'currentAmount', 'productionRate'], + }, + }) + .send() + .expect(HttpStatus.OK); + + const resourcesFromResponse = response.body.data.resource_getAll; + + expect(resourcesFromResponse).toHaveResourceWithCustomProperty({ + resourceId: 'wood', + value: 1, + property: 'productionRate', + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'wood', + minValue: 23, + maxValue: 26, + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'stone_granite', + value: 0, + }); + expect(resourcesFromResponse).toHaveResourceWithValue({ + resourceId: 'coal', + value: 5, + }); + }, 9999999); + }); + }); +}); diff --git a/test/e2e/buildings/building-list.spec.ts b/test/e2e/buildings/building-list.spec.ts index 749e4de..796b170 100644 --- a/test/e2e/buildings/building-list.spec.ts +++ b/test/e2e/buildings/building-list.spec.ts @@ -22,8 +22,8 @@ describe('Get list of all buildings', () => { fields: ['name'], }, }) - .send() - .expect(HttpStatus.OK); + .send(); + // .expect(HttpStatus.OK); expect(response.body.errors[0].message).toBe('Unauthorized'); expect(response.body.data.building_getAll).toBeNull(); diff --git a/test/expect-extend/resource-assert.ts b/test/expect-extend/resource-assert.ts index 028f278..5b1090f 100644 --- a/test/expect-extend/resource-assert.ts +++ b/test/expect-extend/resource-assert.ts @@ -1,10 +1,18 @@ import CustomMatcherResult = jest.CustomMatcherResult; import {HabitatResourceCombined} from '@warp-core/database/model/habitat-resource.mapped.model'; -export type ResourceCheck = { - resourceId: string; +type ExactResourceCheck = { value: number; }; + +type RangeResourceCheck = { + minValue: number; + maxValue: number; +}; + +export type ResourceCheck = { + resourceId: string; +} & (ExactResourceCheck | RangeResourceCheck); export type ResourceWithCustomPropertyCheck = { resourceId: string; property: string; @@ -76,17 +84,47 @@ export function toHaveResourceWithValue( return resourceAssert; } - const {resourceId, value} = actual; + const {resourceId} = actual; const resourceToCheck = getResourceFromReceivedResources( received as HabitatResourceCombined[], resourceId, )!; + if ('value' in actual) { + return checkExactResourceValue(resourceToCheck, actual.value); + } + + return checkResourceValueInRange( + resourceToCheck, + actual.minValue, + actual.maxValue, + ); +} + +function checkExactResourceValue( + resourceToCheck: Partial, + exactValue: number, +): CustomMatcherResult { + return { + pass: exactValue === resourceToCheck.currentAmount, + message: () => + `Resource "${resourceToCheck.resourceId}" amount should be ${exactValue}, actual value is ${resourceToCheck.currentAmount}`, + }; +} + +function checkResourceValueInRange( + resourceToCheck: Partial, + minValue: number, + maxValue: number, +): CustomMatcherResult { + const currentAmount = resourceToCheck.currentAmount ?? 0; + const minValuePass = minValue <= currentAmount; + const maxValuePass = maxValue >= currentAmount; return { - pass: value === resourceToCheck.currentAmount, + pass: minValuePass === maxValuePass, message: () => - `Resource "${resourceId}" amount should be ${value}, actual value is ${resourceToCheck.currentAmount}`, + `Resource "${resourceToCheck.resourceId}" amount should be between ${minValue} and ${maxValue}, actual value is ${resourceToCheck.currentAmount}`, }; }