diff --git a/.gitignore b/.gitignore index b8d80d2..c7d2637 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ adminsetting.json # ignore local appsettings with secrets appsettings.json appsettings.Development.json +adminsettings.json # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/Project.App/Project.Api/Controllers/UserController.cs b/Project.App/Project.Api/Controllers/UserController.cs index 123c790..c01971b 100644 --- a/Project.App/Project.Api/Controllers/UserController.cs +++ b/Project.App/Project.Api/Controllers/UserController.cs @@ -10,18 +10,18 @@ namespace Project.Api.Controllers [Route("api/[controller]")] public class UserController : ControllerBase { - private readonly IUserRepository _userRepository; + private readonly IUserService _userService; - public UserController(IUserRepository userRepository) + public UserController(IUserService userService) { - _userRepository = userRepository; + _userService = userService; } // GET: api/user [HttpGet] public async Task>> GetAllUsers() { - var users = await _userRepository.GetAllAsync(); + var users = await _userService.GetAllUsersAsync(); return Ok(users); } @@ -29,7 +29,7 @@ public async Task>> GetAllUsers() [HttpGet("{id}")] public async Task> GetUserById(Guid id) { - var user = await _userRepository.GetByIdAsync(id); + var user = await _userService.GetUserByIdAsync(id); if (user == null) return NotFound($"User with ID {id} not found."); return Ok(user); @@ -42,7 +42,7 @@ public async Task> CreateUser([FromBody] User user) if (!ModelState.IsValid) return BadRequest(ModelState); - await _userRepository.AddAsync(user); + await _userService.CreateUserAsync(user); return CreatedAtAction(nameof(GetUserById), new { id = user.Id }, user); } @@ -53,7 +53,7 @@ public async Task UpdateUser(Guid id, [FromBody] User user) if (id != user.Id) return BadRequest("User ID mismatch."); - var existingUser = await _userRepository.GetByIdAsync(id); + var existingUser = await _userService.GetUserByIdAsync(id); if (existingUser == null) return NotFound($"User with ID {id} not found."); @@ -62,7 +62,34 @@ public async Task UpdateUser(Guid id, [FromBody] User user) existingUser.Email = user.Email; existingUser.Balance = user.Balance; - await _userRepository.UpdateAsync(existingUser); + await _userService.UpdateUserAsync(id, existingUser); + return NoContent(); + } + + // PATCH: api/user/{id} + [HttpPatch("{id}")] + [Authorize] + public async Task UpdateBalance( + Guid id, + [FromBody] UpdateBalanceRequest body + ) + { + if (body is null) + return BadRequest("Missing body."); + + // only allow the logged-in user to change their own balance + var emailClaim = User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value; + if (string.IsNullOrWhiteSpace(emailClaim)) + return Unauthorized(); + + var me = await _userService.GetUserByEmailAsync(emailClaim); + if (me is null) + return Unauthorized(); + + if (me.Id != id) + return Forbid(); // 403 if trying to edit someone else + + await _userService.UpdateUserBalanceAsync(id, body.Balance); return NoContent(); } @@ -70,11 +97,11 @@ public async Task UpdateUser(Guid id, [FromBody] User user) [HttpDelete("{id}")] public async Task DeleteUser(Guid id) { - var existingUser = await _userRepository.GetByIdAsync(id); + var existingUser = await _userService.GetUserByIdAsync(id); if (existingUser == null) return NotFound($"User with ID {id} not found."); - await _userRepository.DeleteAsync(id); + await _userService.DeleteUserAsync(id); return NoContent(); } @@ -87,7 +114,7 @@ public async Task Me([FromServices] IUserService users) if (email is null) return Unauthorized(); - var u = await users.GetByEmailAsync(email.Value); + var u = await users.GetUserByEmailAsync(email.Value); if (u is null) return NotFound(); //temporary user dto since we dont have one made @@ -103,4 +130,6 @@ public async Task Me([FromServices] IUserService users) ); } } + + public record UpdateBalanceRequest(double Balance); } diff --git a/Project.App/Project.Api/Program.cs b/Project.App/Project.Api/Program.cs index 15c77aa..112705f 100644 --- a/Project.App/Project.Api/Program.cs +++ b/Project.App/Project.Api/Program.cs @@ -1,5 +1,9 @@ +using System.Security.Claims; +using System.Text.Json; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; +using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.EntityFrameworkCore; using Project.Api.Data; using Project.Api.Repositories; @@ -45,9 +49,27 @@ public static void Main(string[] args) var app = builder.Build(); + // optional/safe auto-migrate in other words our startup wont crash if the our migration model doenst match the DB + using (var scope = app.Services.CreateScope()) + { + var db = scope.ServiceProvider.GetRequiredService(); + try + { + db.Database.Migrate(); + Log.Information("[Startup] Database migration check complete."); + } + catch (Exception ex) + { + Log.Warning( + ex, + "[Startup] Database migration skipped or failed. Continuing startup." + ); + } + } + app.UseMiddleware(); - // map default endpoint (shows connection string) + // connection string print app.MapGet( "/string", () => @@ -57,7 +79,7 @@ public static void Main(string[] args) } ); - // Configure the HTTP request pipeline. + // is development envrionment if (app.Environment.IsDevelopment()) { app.MapOpenApi(); @@ -175,13 +197,14 @@ IWebHostEnvironment environment // where the app reads identity from on each request options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // how the app prompts an unauthenticated user to log in + // you can set DefaultChallengeScheme to Cookies instead but it didnt work before options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; }) .AddCookie(cookie => { // cross-site cookie SPA on :3000 to API on :7069 cookie.Cookie.SameSite = SameSiteMode.None; - // browsers require Secure when SameSite=None this is why we need https instead of http + // browsers require Secure when SameSite=None cookie.Cookie.SecurePolicy = CookieSecurePolicy.Always; // for APIs return status codes instead of 302 redirects @@ -201,37 +224,96 @@ IWebHostEnvironment environment }) .AddGoogle(options => { - options.ClientId = configuration["Google:ClientId"]!; // from secrets / config - options.ClientSecret = configuration["Google:ClientSecret"]!; // from secrets / config - options.CallbackPath = "/auth/google/callback"; // google redirects here after login if we change this we need to change it on google cloud as well! - //options.Events = new OAuthEvents - { //TEMPORARILY COMMENTED OUR BELOW BECAUSE IT WAS MESSING WITH GOOGLE AUTH LOGIN FOR SOME REASON GOTTA CHECK THIS LATER - // OnCreatingTicket = async ctx => //currently we are accessing the user json that we get back from google oauth and using it as a quick validation check since email is our unique primary identified on users rn - { /* - var email = ctx.User.GetProperty("email").GetString(); //all of these checks are quick validation can be moved elsewhere when we decide where to put it + options.ClientId = configuration["Google:ClientId"]!; + options.ClientSecret = configuration["Google:ClientSecret"]!; + options.CallbackPath = "/auth/google/callback"; + options.Scope.Add("email"); + options.Scope.Add("profile"); + + // ensure we actually fetch profile claims from Google UserInfo endpoint + options.SaveTokens = true; + options.UserInformationEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo"; + options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + options.ClaimActions.MapJsonKey("picture", "picture"); + options.ClaimActions.MapJsonKey("email_verified", "email_verified"); + + // Upsert user into DB when Google signs in successfully + options.Events = new OAuthEvents + { + OnCreatingTicket = async ctx => + { + var log = ctx.HttpContext.RequestServices.GetRequiredService< + ILogger + >(); + + // fetch the full user JSON from Google's userinfo endpoint + var request = new System.Net.Http.HttpRequestMessage( + System.Net.Http.HttpMethod.Get, + ctx.Options.UserInformationEndpoint + ); + request.Headers.Authorization = + new System.Net.Http.Headers.AuthenticationHeaderValue( + "Bearer", + ctx.AccessToken + ); + + var response = await ctx.Backchannel.SendAsync(request); + response.EnsureSuccessStatusCode(); + + using var userJson = JsonDocument.Parse( + await response.Content.ReadAsStringAsync() + ); + var j = userJson.RootElement; + + var email = j.TryGetProperty("email", out var e) ? e.GetString() : null; var verified = - ctx.User.TryGetProperty("email_verified", out var ev) - && ev.GetBoolean(); - var name = ctx.User.TryGetProperty("name", out var n) - ? n.GetString() - : null; - var picture = ctx.User.TryGetProperty("picture", out var p) - ? p.GetString() - : null; + j.TryGetProperty("email_verified", out var v) && v.GetBoolean(); + var name = j.TryGetProperty("name", out var n) ? n.GetString() : null; + var picture = j.TryGetProperty("picture", out var p) ? p.GetString() : null; + + log.LogInformation( + "[OAuth] CreatingTicket: email={Email}, verified={Verified}, name={Name}", + email, + verified, + name + ); if (string.IsNullOrWhiteSpace(email) || !verified) { + log.LogWarning("[OAuth] Missing or unverified email; failing ticket."); ctx.Fail("Google email must be present and verified."); return; } - var svc = - ctx.HttpContext.RequestServices.GetRequiredService(); - await svc.UpsertGoogleUserByEmailAsync(email, name, picture); - */ - } - } - ; + try + { + using var scope = ctx.HttpContext.RequestServices.CreateScope(); + var users = scope.ServiceProvider.GetRequiredService(); + + var user = await users.UpsertGoogleUserByEmailAsync( + email!, + name, + picture + ); + + log.LogInformation( + "[OAuth] Upsert complete: UserId={UserId}, Email={UserEmail}", + user.Id, + user.Email + ); + } + catch (Exception ex) + { + log.LogError( + ex, + "[OAuth] Error during UpsertGoogleUserByEmailAsync for {Email}", + email + ); + ctx.Fail("Internal error during user creation."); + } + }, + }; }); services.AddAuthorization(); diff --git a/Project.App/Project.Api/Repositories/Interface/IUserRepository.cs b/Project.App/Project.Api/Repositories/Interface/IUserRepository.cs index 2d57fcd..9e96e18 100644 --- a/Project.App/Project.Api/Repositories/Interface/IUserRepository.cs +++ b/Project.App/Project.Api/Repositories/Interface/IUserRepository.cs @@ -1,3 +1,4 @@ +using System.Reflection.Metadata; using Project.Api.Models; namespace Project.Api.Repositories.Interface @@ -9,6 +10,7 @@ public interface IUserRepository Task GetByEmailAsync(string email); Task AddAsync(User user); Task UpdateAsync(User user); + Task UpdateBalanceAsync(User user); Task DeleteAsync(Guid id); Task SaveChangesAsync(); } diff --git a/Project.App/Project.Api/Repositories/UserRepository.cs b/Project.App/Project.Api/Repositories/UserRepository.cs index f9541ba..82d2389 100644 --- a/Project.App/Project.Api/Repositories/UserRepository.cs +++ b/Project.App/Project.Api/Repositories/UserRepository.cs @@ -49,6 +49,13 @@ public async Task UpdateAsync(User user) await _context.SaveChangesAsync(); } + // Update user balance + public async Task UpdateBalanceAsync(User user) + { + _context.Users.Update(user); + await _context.SaveChangesAsync(); + } + // Delete user by ID public async Task DeleteAsync(Guid id) { diff --git a/Project.App/Project.Api/Services/UserService.cs b/Project.App/Project.Api/Services/UserService.cs index baaef45..5d83daf 100644 --- a/Project.App/Project.Api/Services/UserService.cs +++ b/Project.App/Project.Api/Services/UserService.cs @@ -117,7 +117,7 @@ public async Task UpdateUserBalanceAsync(Guid userId, double newBalance) } user.Balance = newBalance; - await _repo.UpdateAsync(user); + await _repo.UpdateBalanceAsync(user); return user; } catch (Exception e) diff --git a/Project.App/Project.Client/src/app/components/AddCreditsModal.js b/Project.App/Project.Client/src/app/components/AddCreditsModal.js index 9641ef3..bbdcd9a 100644 --- a/Project.App/Project.Client/src/app/components/AddCreditsModal.js +++ b/Project.App/Project.Client/src/app/components/AddCreditsModal.js @@ -23,6 +23,7 @@ export default function AddCreditsModal({ isOpen, onClose, balance, creditsToAdd type="number" step="10" min="0" + max="200000" value={creditsToAdd} onChange={(e) => setCreditsToAdd(e.target.value)} placeholder="0" diff --git a/Project.App/Project.Client/src/app/components/NavBar.js b/Project.App/Project.Client/src/app/components/NavBar.js index f0440d5..45e3175 100644 --- a/Project.App/Project.Client/src/app/components/NavBar.js +++ b/Project.App/Project.Client/src/app/components/NavBar.js @@ -43,7 +43,7 @@ export default function NavBar() { if (pathname.startsWith('/player/')) { links = [{ href: '/rooms', label: 'Rooms' }]; } else if (pathname.startsWith('/rooms')) { - links = [{ href: '/player/1', label: 'Profile' }]; + links = [{ href: '/player/profile', label: 'Profile' }]; } else if (pathname.startsWith('/game/')) { links = [{ href: '/rooms', label: 'Leave Room' }]; } diff --git a/Project.App/Project.Client/src/app/player/[id]/PlayerClient.jsx b/Project.App/Project.Client/src/app/player/[id]/PlayerClient.jsx index a008069..84060d7 100644 --- a/Project.App/Project.Client/src/app/player/[id]/PlayerClient.jsx +++ b/Project.App/Project.Client/src/app/player/[id]/PlayerClient.jsx @@ -4,10 +4,12 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import AddCreditsModal from '../../components/AddCreditsModal'; -export default function PlayerClient({ _id, initialBalance }) { +export default function PlayerClient({ _id }) { const router = useRouter(); - const [playerName] = useState('Danny Devito'); - const [balance, setBalance] = useState(initialBalance ?? 1000); + const [playerName, setPlayerName] = useState(''); + const [playerId, setPlayerId] = useState(''); + const [playerPfp, setAvatarUrl] = useState(''); + const [balance, setBalance] = useState(0); const [showModal, setShowModal] = useState(false); const [creditsToAdd, setCreditsToAdd] = useState(''); @@ -19,25 +21,61 @@ export default function PlayerClient({ _id, initialBalance }) { if (!res.ok) { router.replace('/login'); } else { - res.json().then((data) => { - console.log('Authenticated user:', data); + res.json().then(() => { + // authenticated successfully }); } }) - .catch((err) => { - console.error('Auth check failed:', err); + .catch(() => { router.replace('/login'); }); + + fetch(`${apiBaseUrl}/api/user/me`, { credentials: 'include' }) + .then((res) => { + if (!res.ok) { + router.replace('/rooms'); + } else { + res.json().then((data) => { + setPlayerId(data.id); + setPlayerName(data.name); + setBalance(data.balance); + setAvatarUrl(data.avatarUrl); + }); + } + }); }, [router]); - const handleAddCredits = (e) => { + const handleAddCredits = async (e) => { e.preventDefault(); + const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || 'https://localhost:7069'; + const amount = parseFloat(creditsToAdd); - if (!isNaN(amount) && amount > 0) { - setBalance((prev) => prev + amount); + if (Number.isNaN(amount) || amount <= 0) return; + + const newBalance = balance + amount; + + try { + const res = await fetch(`${apiBaseUrl}/api/user/${playerId}`, { + method: 'PATCH', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ balance: newBalance }), + }); + + if (!res.ok) { + if (res.status === 401) { + router.replace('/login'); + } + return; + } + + setBalance(newBalance); setCreditsToAdd(''); setShowModal(false); - // Later: PATCH to backend with new balance for player {id} + } catch { + // intentionally left blank to satisfy lint rules } }; @@ -49,19 +87,22 @@ export default function PlayerClient({ _id, initialBalance }) { return (
- {/* Player Name */} + Gamer Avatar

{playerName}

- {/* Player Credits */}

Current Credits

{balance} Devito Bucks

- {/* Add Credits Button */} diff --git a/Project.App/Project.Client/src/app/player/[id]/page.js b/Project.App/Project.Client/src/app/player/[id]/page.js index 55ee813..586de97 100644 --- a/Project.App/Project.Client/src/app/player/[id]/page.js +++ b/Project.App/Project.Client/src/app/player/[id]/page.js @@ -1,8 +1,6 @@ import PlayerClient from './PlayerClient'; -export default function PlayerProfile({ params }) { - const { id } = params; - +export default function PlayerProfile() { // Auth check is handled by client-side guard in PlayerClient - return ; -} + return ; +} \ No newline at end of file diff --git a/Project.App/Project.Test/Controllers/UserControllerTest.cs b/Project.App/Project.Test/Controllers/UserControllerTest.cs index 209d6e5..38432e5 100644 --- a/Project.App/Project.Test/Controllers/UserControllerTest.cs +++ b/Project.App/Project.Test/Controllers/UserControllerTest.cs @@ -2,19 +2,19 @@ using Moq; using Project.Api.Controllers; using Project.Api.Models; -using Project.Api.Repositories.Interface; +using Project.Api.Services.Interface; namespace Project.Api.Tests { public class UserControllerTests { - private readonly Mock _mockRepo; + private readonly Mock _mockSvc; private readonly UserController _controller; public UserControllerTests() { - _mockRepo = new Mock(); - _controller = new UserController(_mockRepo.Object); + _mockSvc = new Mock(); + _controller = new UserController(_mockSvc.Object); } [Fact] @@ -36,7 +36,7 @@ public async Task GetAllUsers_ReturnsOkResult_WithUsers() Email = "leo@example.com", }, }; - _mockRepo.Setup(repo => repo.GetAllAsync()).ReturnsAsync(users); + _mockSvc.Setup(s => s.GetAllUsersAsync()).ReturnsAsync(users); // Act var result = await _controller.GetAllUsers(); @@ -58,7 +58,7 @@ public async Task GetUserById_ReturnsOk_WhenUserExists() Name = "Sneha", Email = "sneha@example.com", }; - _mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(user); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(user); // Act var result = await _controller.GetUserById(userId); @@ -74,7 +74,7 @@ public async Task GetUserById_ReturnsNotFound_WhenUserDoesNotExist() { // Arrange var userId = Guid.NewGuid(); - _mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync((User?)null); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync((User?)null); // Act var result = await _controller.GetUserById(userId); @@ -93,7 +93,7 @@ public async Task CreateUser_ReturnsCreatedAtAction() Name = "Sneha", Email = "sneha@example.com", }; - _mockRepo.Setup(r => r.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.CreateUserAsync(It.IsAny())).ReturnsAsync((User u) => u); // Act var result = await _controller.CreateUser(user); @@ -112,8 +112,10 @@ public async Task UpdateUser_ReturnsNoContent_WhenSuccessful() var existingUser = new User { Id = userId, Name = "Old Name" }; var updatedUser = new User { Id = userId, Name = "New Name" }; - _mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(existingUser); - _mockRepo.Setup(r => r.UpdateAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(existingUser); + _mockSvc + .Setup(s => s.UpdateUserAsync(userId, It.IsAny())) + .ReturnsAsync(updatedUser); // Act var result = await _controller.UpdateUser(userId, updatedUser); @@ -127,10 +129,15 @@ public async Task DeleteUser_ReturnsNoContent_WhenSuccessful() { // Arrange var userId = Guid.NewGuid(); - var user = new User { Id = userId, Name = "Sneha" }; + var user = new Project.Api.Models.User + { + Id = userId, + Name = "Sneha", + Email = "sneha@example.com", + }; - _mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(user); - _mockRepo.Setup(r => r.DeleteAsync(userId)).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(user); + _mockSvc.Setup(s => s.DeleteUserAsync(userId)).ReturnsAsync(true); // Act var result = await _controller.DeleteUser(userId); @@ -144,7 +151,7 @@ public async Task DeleteUser_ReturnsNotFound_WhenUserDoesNotExist() { // Arrange var userId = Guid.NewGuid(); - _mockRepo.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync((User?)null); + _mockSvc.Setup(s => s.DeleteUserAsync(userId)).ReturnsAsync(false); // Act var result = await _controller.DeleteUser(userId); diff --git a/Project.App/Project.Test/Controllers/UserControllerUnitTest.cs b/Project.App/Project.Test/Controllers/UserControllerUnitTest.cs index cece10e..1df723d 100644 --- a/Project.App/Project.Test/Controllers/UserControllerUnitTest.cs +++ b/Project.App/Project.Test/Controllers/UserControllerUnitTest.cs @@ -2,19 +2,19 @@ using Moq; using Project.Api.Controllers; using Project.Api.Models; -using Project.Api.Repositories.Interface; +using Project.Api.Services.Interface; namespace Project.Tests.Controllers { public class UserControllerTests { - private readonly Mock _mockRepo; + private readonly Mock _mockSvc; private readonly UserController _controller; public UserControllerTests() { - _mockRepo = new Mock(); - _controller = new UserController(_mockRepo.Object); + _mockSvc = new Mock(); + _controller = new UserController(_mockSvc.Object); } [Fact] @@ -36,7 +36,7 @@ public async Task GetAllUsers_ReturnsOkResult_WithListOfUsers() Email = "leo@example.com", }, }; - _mockRepo.Setup(repo => repo.GetAllAsync()).ReturnsAsync(users); + _mockSvc.Setup(s => s.GetAllUsersAsync()).ReturnsAsync(users); // Act var result = await _controller.GetAllUsers(); @@ -58,7 +58,7 @@ public async Task GetUserById_ReturnsOk_WhenUserExists() Name = "Sneha", Email = "sneha@example.com", }; - _mockRepo.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync(user); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(user); // Act var result = await _controller.GetUserById(userId); @@ -74,7 +74,7 @@ public async Task GetUserById_ReturnsNotFound_WhenUserDoesNotExist() { // Arrange var userId = Guid.NewGuid(); - _mockRepo.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync((User?)null); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync((User?)null); // Act var result = await _controller.GetUserById(userId); @@ -93,7 +93,7 @@ public async Task CreateUser_ReturnsCreatedAtAction() Name = "Sneha", Email = "sneha@example.com", }; - _mockRepo.Setup(repo => repo.AddAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.CreateUserAsync(It.IsAny())).ReturnsAsync((User u) => u); // Act var result = await _controller.CreateUser(user); @@ -112,8 +112,10 @@ public async Task UpdateUser_ReturnsNoContent_WhenSuccessful() var existingUser = new User { Id = userId, Name = "Old Name" }; var updatedUser = new User { Id = userId, Name = "New Name" }; - _mockRepo.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync(existingUser); - _mockRepo.Setup(repo => repo.UpdateAsync(It.IsAny())).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(existingUser); + _mockSvc + .Setup(s => s.UpdateUserAsync(userId, It.IsAny())) + .ReturnsAsync(updatedUser); // Act var result = await _controller.UpdateUser(userId, updatedUser); @@ -125,17 +127,19 @@ public async Task UpdateUser_ReturnsNoContent_WhenSuccessful() [Fact] public async Task DeleteUser_ReturnsNoContent_WhenSuccessful() { - // Arrange var userId = Guid.NewGuid(); - var user = new User { Id = userId, Name = "Sneha" }; + var user = new Project.Api.Models.User + { + Id = userId, + Name = "Sneha", + Email = "sneha@example.com", + }; - _mockRepo.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync(user); - _mockRepo.Setup(repo => repo.DeleteAsync(userId)).Returns(Task.CompletedTask); + _mockSvc.Setup(s => s.GetUserByIdAsync(userId)).ReturnsAsync(user); + _mockSvc.Setup(s => s.DeleteUserAsync(userId)).ReturnsAsync(true); - // Act var result = await _controller.DeleteUser(userId); - // Assert Assert.IsType(result); } @@ -144,7 +148,7 @@ public async Task DeleteUser_ReturnsNotFound_WhenUserDoesNotExist() { // Arrange var userId = Guid.NewGuid(); - _mockRepo.Setup(repo => repo.GetByIdAsync(userId)).ReturnsAsync((User?)null); + _mockSvc.Setup(s => s.DeleteUserAsync(userId)).ReturnsAsync(false); // Act var result = await _controller.DeleteUser(userId); diff --git a/Project.App/Project.Test/Services/UserServiceTest.cs b/Project.App/Project.Test/Services/UserServiceTest.cs index b1a389c..43d04eb 100644 --- a/Project.App/Project.Test/Services/UserServiceTest.cs +++ b/Project.App/Project.Test/Services/UserServiceTest.cs @@ -203,21 +203,26 @@ public async Task UpdateUserBalance_ReturnsUpdatedUser_WhenSuccessful() { // Arrange var userId = Guid.NewGuid(); - var user = RepositoryTestHelper.CreateTestUser(id: userId, balance: 1000); + var startingUser = RepositoryTestHelper.CreateTestUser(id: userId, balance: 1000.0); var newBalance = 2000.0; - _userRepositoryMock.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(user); - _userRepositoryMock.Setup(r => r.UpdateAsync(It.IsAny())).Returns(Task.CompletedTask); + _userRepositoryMock.Setup(r => r.GetByIdAsync(userId)).ReturnsAsync(startingUser); + + _userRepositoryMock + .Setup(r => r.UpdateBalanceAsync(It.IsAny())) + .Returns(Task.CompletedTask); // Act var result = await _service.UpdateUserBalanceAsync(userId, newBalance); // Assert result.Should().NotBeNull(); - result.Id.Should().Be(userId); + result!.Id.Should().Be(userId); result.Balance.Should().Be(newBalance); + + _userRepositoryMock.Verify(r => r.GetByIdAsync(userId), Times.Once); _userRepositoryMock.Verify( - r => r.UpdateAsync(It.Is(u => u.Id == userId && u.Balance == newBalance)), + r => r.UpdateBalanceAsync(It.Is(u => u.Id == userId && u.Balance == newBalance)), Times.Once ); }