diff --git a/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts new file mode 100644 index 000000000..cfd60b0dc --- /dev/null +++ b/src/__tests__/clanInventory/stock/StockService/getStockClanId.test.ts @@ -0,0 +1,78 @@ +import { ObjectId } from 'mongodb'; +import ClanInventoryBuilderFactory from '../../data/clanInventoryBuilderFactory'; +import StockModule from '../../modules/stock.module'; +import { StockService } from '../../../../clanInventory/stock/stock.service'; +import { getNonExisting_id } from '../../../test_utils/util/getNonExisting_id'; + +describe('StockService.getStockClanId() test suite', () => { + let stockService: StockService; + const stockBuilder = ClanInventoryBuilderFactory.getBuilder('Stock'); + const stockModel = StockModule.getStockModel(); + + beforeEach(async () => { + stockService = await StockModule.getStockService(); + }); + + it('Should find clan _id of the stock', async () => { + const clan_id = new ObjectId(); + const stockToCreate = stockBuilder.setClanId(clan_id).build(); + const createdStock = await stockModel.create(stockToCreate); + + const [stockClan_id, errors] = await stockService.getStockClanId( + createdStock._id, + ); + + expect(errors).toBeNull(); + expect(stockClan_id).toBe(clan_id.toString()); + }); + + it('Should return the clan _id as a string', async () => { + const clan_id = new ObjectId(); + const stockToCreate = stockBuilder.setClanId(clan_id).build(); + const createdStock = await stockModel.create(stockToCreate); + + const [stockClan_id] = await stockService.getStockClanId(createdStock._id); + + expect(typeof stockClan_id).toBe('string'); + }); + + it('Should find clan _id of the specified stock only', async () => { + const clan1_id = new ObjectId(); + const clan2_id = new ObjectId(); + + const createdStock1 = await stockModel.create( + stockBuilder.setClanId(clan1_id).build(), + ); + const createdStock2 = await stockModel.create( + stockBuilder.setClanId(clan2_id).build(), + ); + + const [stock1Clan_id, stock1Errors] = await stockService.getStockClanId( + createdStock1._id, + ); + const [stock2Clan_id, stock2Errors] = await stockService.getStockClanId( + createdStock2._id, + ); + + expect(stock1Errors).toBeNull(); + expect(stock2Errors).toBeNull(); + expect(stock1Clan_id).toBe(clan1_id.toString()); + expect(stock2Clan_id).toBe(clan2_id.toString()); + }); + + it('Should return NOT_FOUND SE if stock with provided _id does not exists', async () => { + const [stockClan_id, errors] = + await stockService.getStockClanId(getNonExisting_id()); + + expect(stockClan_id).toBeNull(); + expect(errors).toContainSE_NOT_FOUND(); + }); + + it('Should return VALIDATION SE if provided _id param is not a mongo id', async () => { + const [stockClan_id, errors] = + await stockService.getStockClanId('not-mongo-id'); + + expect(stockClan_id).toBeNull(); + expect(errors).toContainSE_VALIDATION(); + }); +}); diff --git a/src/clanInventory/item/item.controller.ts b/src/clanInventory/item/item.controller.ts index 967a22f77..d5943663a 100644 --- a/src/clanInventory/item/item.controller.ts +++ b/src/clanInventory/item/item.controller.ts @@ -87,12 +87,32 @@ export class ItemController { dto: ItemDto, modelName: ModelName.ITEM, }, - errors: [400, 404], + errors: [400, 403, 404], }) @Get('/:_id') @Authorize({ action: Action.read, subject: ItemDto }) @UniformResponse(ModelName.ITEM) - public get(@Param() param: _idDto) { + public async get(@Param() param: _idDto, @LoggedUser() user: User) { + const [itemClan_id, errors] = await this.itemHelper.getItemClanId( + param._id, + ); + if (errors || !itemClan_id) return [null, errors]; + + const playerClan = await this.playerModel.findById(user.player_id); + + if (playerClan.clan_id.toString() !== itemClan_id) + return [ + null, + [ + new APIError({ + reason: APIErrorReason.NOT_AUTHORIZED, + field: '_id', + value: param._id, + message: "Cannot view another Clan's Items", + }), + ], + ]; + return this.itemService.readOneById(param._id); } diff --git a/src/clanInventory/stock/stock.controller.ts b/src/clanInventory/stock/stock.controller.ts index 0c9efe50d..223d3f096 100644 --- a/src/clanInventory/stock/stock.controller.ts +++ b/src/clanInventory/stock/stock.controller.ts @@ -1,10 +1,15 @@ import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; import { StockService } from './stock.service'; import { StockDto } from './dto/stock.dto'; import { ItemDto } from '../item/dto/item.dto'; import { User } from '../../auth/user'; +import { Player } from '../../player/schemas/player.schema'; import { Authorize } from '../../authorization/decorator/Authorize'; import { Action } from '../../authorization/enum/action.enum'; +import { APIError } from '../../common/controller/APIError'; +import { APIErrorReason } from '../../common/controller/APIErrorReason'; import { GetAllQuery } from '../../common/decorator/param/GetAllQuery'; import { LoggedUser } from '../../common/decorator/param/LoggedUser.decorator'; import { UniformResponse } from '../../common/decorator/response/UniformResponse'; @@ -19,12 +24,15 @@ import { Environment } from '../../common/enum/environment.enum'; @Controller('stock') export class StockController { - public constructor(private readonly service: StockService) {} + public constructor( + private readonly service: StockService, + @InjectModel(ModelName.PLAYER) private readonly playerModel: Model, + ) {} /** - * Get stock items by Stock _id + * Get logged-in Player's Clan stock items by Stock _id * - * @remarks Returns the list of Items currently stored in the Stock with the given _id. + * @remarks Returns the list of Items currently stored in the logged-in Player's Clan Stock with the given _id. */ @ApiResponseDescription({ success: { @@ -32,12 +40,30 @@ export class StockController { modelName: ModelName.ITEM, returnsArray: true, }, - errors: [400, 401, 404], + errors: [400, 401, 403, 404], }) @Get('/:_id') @Authorize({ action: Action.read, subject: StockDto }) @UniformResponse(ModelName.ITEM) - public get(@Param() param: _idDto) { + public async get(@Param() param: _idDto, @LoggedUser() user: User) { + const [stockClan_id, errors] = await this.service.getStockClanId(param._id); + if (errors || !stockClan_id) return [null, errors]; + + const playerClan = await this.playerModel.findById(user.player_id); + + if (playerClan.clan_id.toString() !== stockClan_id) + return [ + null, + [ + new APIError({ + reason: APIErrorReason.NOT_AUTHORIZED, + field: '_id', + value: param._id, + message: "Cannot view another Clan's Stock", + }), + ], + ]; + return this.service.readItemsByStockId(param._id); } diff --git a/src/clanInventory/stock/stock.service.ts b/src/clanInventory/stock/stock.service.ts index 24c5ca07f..d96bf1adb 100644 --- a/src/clanInventory/stock/stock.service.ts +++ b/src/clanInventory/stock/stock.service.ts @@ -116,6 +116,23 @@ export class StockService { return [{ ...stockObject, FleaMarketItem: fleaMarketItems ?? [] }, null]; } + /** + * Retrieves the clan ID that owns the Stock. + * + * @param _id - The Mongo _id of the Clan Stock. + * @returns A promise that resolves to a tuple where the first element is the clan ID (string) or null and the second element is either null or an array of ServiceErrors. + */ + async getStockClanId( + _id: string, + ): Promise<[string | null, ServiceError[] | null]> { + const [stock, errors] = await this.basicService.readOneById(_id, { + select: ['clan_id'], + }); + if (errors) return [null, errors]; + + return [stock.clan_id.toString(), null]; + } + /** * Reads all Items stored in the specified Stock. *