From 4f1342e0afc946beca7428a0f751f8d72614b014 Mon Sep 17 00:00:00 2001 From: hz Date: Wed, 4 Jun 2025 22:03:01 -0500 Subject: [PATCH] Adding initial auction system --- Controllers/AuctionController.cs | 68 +++++++++++++++++++ GameAuthApi.Tests/AuctionControllerTests.cs | 74 +++++++++++++++++++++ Models/Auction.cs | 24 +++++++ Models/DTOs/BidRequest.cs | 7 ++ Models/DTOs/CreateAuctionRequest.cs | 7 ++ Program.cs | 1 + Services/AuctionService.cs | 31 +++++++++ 7 files changed, 212 insertions(+) create mode 100644 Controllers/AuctionController.cs create mode 100644 GameAuthApi.Tests/AuctionControllerTests.cs create mode 100644 Models/Auction.cs create mode 100644 Models/DTOs/BidRequest.cs create mode 100644 Models/DTOs/CreateAuctionRequest.cs create mode 100644 Services/AuctionService.cs diff --git a/Controllers/AuctionController.cs b/Controllers/AuctionController.cs new file mode 100644 index 0000000..eec028a --- /dev/null +++ b/Controllers/AuctionController.cs @@ -0,0 +1,68 @@ +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Security.Claims; +using System.Threading.Tasks; + +namespace GameAuthApi.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class AuctionController : ControllerBase +{ + private readonly AuctionService _auctionService; + private readonly ItemService _itemService; + + public AuctionController(AuctionService auctionService, ItemService itemService) + { + _auctionService = auctionService; + _itemService = itemService; + } + + [HttpPost("list")] + [Authorize(Roles = "USER,SUPER")] + public async Task ListItem([FromBody] CreateAuctionRequest req) + { + var item = await _itemService.GetByIdAsync(req.ItemId); + if (item == null) + return BadRequest("Invalid item ID."); + + var auction = new Auction + { + ItemId = req.ItemId, + StartingPrice = req.StartingPrice, + CurrentPrice = req.StartingPrice, + SellerPlayerId = User.FindFirstValue(ClaimTypes.NameIdentifier) + }; + + await _auctionService.CreateAsync(auction); + return Ok(auction); + } + + [HttpPost("bid")] + [Authorize(Roles = "USER,SUPER")] + public async Task PlaceBid([FromBody] BidRequest req) + { + var auction = await _auctionService.GetByIdAsync(req.AuctionId); + if (auction == null) + return NotFound("Auction not found."); + if (auction.IsClosed) + return BadRequest("Auction closed."); + if (req.BidAmount <= auction.CurrentPrice) + return BadRequest("Bid must be higher than current price."); + + auction.CurrentPrice = req.BidAmount; + auction.HighestBidderId = User.FindFirstValue(ClaimTypes.NameIdentifier); + await _auctionService.UpdateAsync(auction); + return Ok(auction); + } + + [HttpGet("active")] + [Authorize(Roles = "USER,SUPER")] + public async Task GetActive() + { + var active = await _auctionService.GetActiveAsync(); + return Ok(active); + } +} diff --git a/GameAuthApi.Tests/AuctionControllerTests.cs b/GameAuthApi.Tests/AuctionControllerTests.cs new file mode 100644 index 0000000..2f45c52 --- /dev/null +++ b/GameAuthApi.Tests/AuctionControllerTests.cs @@ -0,0 +1,74 @@ +using GameAuthApi.Controllers; +using GameAuthApi.Models; +using GameAuthApi.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System.Security.Claims; + +public class AuctionControllerTests +{ + private static IConfiguration cfg = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + {"MongoDB:ConnectionString","mongodb://localhost"}, + {"MongoDB:DatabaseName","test"} + }).Build(); + + private static ControllerContext Ctx(string id="u") + { + var ctx = new DefaultHttpContext(); + ctx.User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier,id) + }, "mock")); + return new ControllerContext { HttpContext = ctx }; + } + + [Fact] + public async Task ListItem_InvalidItem_ReturnsBadRequest() + { + var itemSvc = new Mock(cfg); + itemSvc.Setup(i => i.GetByIdAsync("bad")).ReturnsAsync((Item?)null); + var controller = new AuctionController(new Mock(cfg).Object, itemSvc.Object) + { + ControllerContext = Ctx() + }; + + var result = await controller.ListItem(new CreateAuctionRequest { ItemId="bad", StartingPrice=1 }); + + Assert.IsType(result); + } + + [Fact] + public async Task PlaceBid_AuctionNotFound() + { + var aucSvc = new Mock(cfg); + aucSvc.Setup(a => a.GetByIdAsync("a")).ReturnsAsync((Auction?)null); + var controller = new AuctionController(aucSvc.Object, new Mock(cfg).Object) + { + ControllerContext = Ctx() + }; + + var result = await controller.PlaceBid(new BidRequest { AuctionId="a", BidAmount=5 }); + + Assert.IsType(result); + } + + [Fact] + public async Task GetActive_ReturnsOk() + { + var aucSvc = new Mock(cfg); + var list = new List(); + aucSvc.Setup(a => a.GetActiveAsync()).ReturnsAsync(list); + var controller = new AuctionController(aucSvc.Object, new Mock(cfg).Object) + { + ControllerContext = Ctx() + }; + + var result = await controller.GetActive(); + + var ok = Assert.IsType(result); + Assert.Equal(list, ok.Value); + } +} diff --git a/Models/Auction.cs b/Models/Auction.cs new file mode 100644 index 0000000..7674339 --- /dev/null +++ b/Models/Auction.cs @@ -0,0 +1,24 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace GameAuthApi.Models; + +public class Auction +{ + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string ItemId { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string SellerPlayerId { get; set; } + + [BsonRepresentation(BsonType.ObjectId)] + public string HighestBidderId { get; set; } + + public int StartingPrice { get; set; } + public int CurrentPrice { get; set; } + public bool IsClosed { get; set; } = false; +} diff --git a/Models/DTOs/BidRequest.cs b/Models/DTOs/BidRequest.cs new file mode 100644 index 0000000..01a1434 --- /dev/null +++ b/Models/DTOs/BidRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class BidRequest +{ + public string AuctionId { get; set; } + public int BidAmount { get; set; } +} diff --git a/Models/DTOs/CreateAuctionRequest.cs b/Models/DTOs/CreateAuctionRequest.cs new file mode 100644 index 0000000..e15038a --- /dev/null +++ b/Models/DTOs/CreateAuctionRequest.cs @@ -0,0 +1,7 @@ +namespace GameAuthApi.Models; + +public class CreateAuctionRequest +{ + public string ItemId { get; set; } + public int StartingPrice { get; set; } +} diff --git a/Program.cs b/Program.cs index a6e829c..24e309b 100644 --- a/Program.cs +++ b/Program.cs @@ -52,6 +52,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var jwtKey = builder.Configuration["Jwt:Key"]; var keyBytes = Encoding.UTF8.GetBytes(jwtKey); diff --git a/Services/AuctionService.cs b/Services/AuctionService.cs new file mode 100644 index 0000000..0d6af43 --- /dev/null +++ b/Services/AuctionService.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using GameAuthApi.Models; +using Microsoft.Extensions.Configuration; +using MongoDB.Driver; + +namespace GameAuthApi.Services; + +public class AuctionService +{ + private readonly IMongoCollection _auctions; + + public AuctionService(IConfiguration config) + { + var client = new MongoClient(config["MongoDB:ConnectionString"]); + var db = client.GetDatabase(config["MongoDB:DatabaseName"]); + _auctions = db.GetCollection("Auctions"); + } + + public virtual async Task CreateAsync(Auction auction) => + await _auctions.InsertOneAsync(auction); + + public virtual async Task GetByIdAsync(string id) => + await _auctions.Find(a => a.Id == id).FirstOrDefaultAsync(); + + public virtual async Task UpdateAsync(Auction auction) => + await _auctions.ReplaceOneAsync(a => a.Id == auction.Id, auction); + + public virtual async Task> GetActiveAsync() => + await _auctions.Find(a => !a.IsClosed).ToListAsync(); +}