diff --git a/backend/backend.Tests/Controllers/EventsControllerTests.cs b/backend/backend.Tests/Controllers/EventsControllerTests.cs new file mode 100644 index 0000000..f653a39 --- /dev/null +++ b/backend/backend.Tests/Controllers/EventsControllerTests.cs @@ -0,0 +1,511 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Moq; +using FluentAssertions; +using RacePlannerApi.Controllers; +using RacePlannerApi.Data; +using RacePlannerApi.DTOs; +using RacePlannerApi.Models; +using backend.Tests.Utilities; + +namespace backend.Tests.Controllers; + +public class EventsControllerTests : IDisposable +{ + private readonly RacePlannerDbContext _context; + private readonly EventsController _controller; + + public EventsControllerTests() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + _context = new RacePlannerDbContext(options); + _controller = new EventsController(_context); + } + + private void SetUserContext(Guid userId, string role = "Participant") + { + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, userId.ToString()), + new Claim(ClaimTypes.Role, role) + }; + var identity = new ClaimsIdentity(claims, "TestAuthType"); + var principal = new ClaimsPrincipal(identity); + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = principal } + }; + } + + public void Dispose() + { + _context.Dispose(); + } + + #region Create Event - Positive Tests + + [Fact] + public async Task CreateEvent_WithValidData_CreatesEvent() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + await _context.SaveChangesAsync(); + SetUserContext(organizer.Id, "Organizer"); + + var request = new CreateEventRequest + { + Name = "Test Marathon", + Description = "A test marathon event", + EventDate = DateTime.UtcNow.AddDays(30), + Location = "Test City", + Category = "Running", + Tags = new List { "marathon", "running" }, + MaxParticipants = 100 + }; + + // Act + var result = await _controller.CreateEvent(request); + + // Assert + var createdResult = result.Result.Should().BeOfType().Subject; + var response = createdResult.Value.Should().BeOfType().Subject; + response.Name.Should().Be(request.Name); + response.Status.Should().Be("Draft"); + response.Organizer.Id.Should().Be(organizer.Id); + } + + [Fact] + public async Task CreateEvent_SetsStatusToDraft() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + await _context.SaveChangesAsync(); + SetUserContext(organizer.Id, "Organizer"); + + var request = new CreateEventRequest + { + Name = "Test Event", + EventDate = DateTime.UtcNow.AddDays(10), + Location = "Test Location", + MaxParticipants = 50 + }; + + // Act + var result = await _controller.CreateEvent(request); + + // Assert + var createdResult = result.Result.Should().BeOfType().Subject; + var response = createdResult.Value.Should().BeOfType().Subject; + response.Status.Should().Be("Draft"); + } + + #endregion + + #region Create Event - Negative Tests + + [Fact(Skip = "Authorization attributes require integration tests with full ASP.NET Core pipeline")] + public async Task CreateEvent_WithoutOrganizerRole_ReturnsForbidden() + { + // Arrange + var participant = TestDataFactory.CreateUser(role: UserRole.Participant); + _context.Users.Add(participant); + await _context.SaveChangesAsync(); + SetUserContext(participant.Id, "Participant"); + + var request = new CreateEventRequest + { + Name = "Test Event", + EventDate = DateTime.UtcNow.AddDays(10), + Location = "Test Location" + }; + + // Act + var result = await _controller.CreateEvent(request); + + // Assert + result.Result.Should().BeOfType(); + } + + [Fact] + public async Task CreateEvent_WithUnauthenticatedUser_ReturnsUnauthorized() + { + // Arrange + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + var request = new CreateEventRequest + { + Name = "Test Event", + EventDate = DateTime.UtcNow.AddDays(10), + Location = "Test Location" + }; + + // Act + var result = await _controller.CreateEvent(request); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Get Events - Positive Tests + + [Fact] + public async Task GetEvents_ReturnsPublishedEventsForAnonymous() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var publishedEvent = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Published); + var draftEvent = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Draft); + _context.Events.AddRange(publishedEvent, draftEvent); + await _context.SaveChangesAsync(); + + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + // Act + var result = await _controller.GetEvents(); + + // Assert + var okResult = result.Result.Should().BeOfType().Subject; + var events = okResult.Value.Should().BeAssignableTo>().Subject; + events.Should().HaveCount(1); + events.First().Status.Should().Be("Published"); + } + + [Fact] + public async Task GetEvents_WithCategoryFilter_ReturnsFilteredEvents() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var runningEvent = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Published); + runningEvent.Category = "Running"; + var cyclingEvent = TestDataFactory.CreateEvent(name: "Cycling Event", organizerId: organizer.Id, status: EventStatus.Published); + cyclingEvent.Category = "Cycling"; + + _context.Events.AddRange(runningEvent, cyclingEvent); + await _context.SaveChangesAsync(); + + // Set anonymous context + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + var filter = new EventFilterRequest { Category = "Running" }; + + // Act + var result = await _controller.GetEvents(filter); + + // Assert + var okResult = result.Result.Should().BeOfType().Subject; + var events = okResult.Value.Should().BeAssignableTo>().Subject; + events.Should().HaveCount(1); + events.First().Category.Should().Be("Running"); + } + + [Fact] + public async Task GetEvents_WithDateRangeFilter_ReturnsFilteredEvents() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var futureEvent = TestDataFactory.CreateEvent( + name: "Future Event", + eventDate: DateTime.UtcNow.AddDays(60), + organizerId: organizer.Id, + status: EventStatus.Published); + var pastEvent = TestDataFactory.CreateEvent( + name: "Past Event", + eventDate: DateTime.UtcNow.AddDays(-10), + organizerId: organizer.Id, + status: EventStatus.Published); + + _context.Events.AddRange(futureEvent, pastEvent); + await _context.SaveChangesAsync(); + + // Set anonymous context + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + var filter = new EventFilterRequest + { + FromDate = DateTime.UtcNow.AddDays(1), + ToDate = DateTime.UtcNow.AddDays(100) + }; + + // Act + var result = await _controller.GetEvents(filter); + + // Assert + var okResult = result.Result.Should().BeOfType().Subject; + var events = okResult.Value.Should().BeAssignableTo>().Subject; + events.Should().HaveCount(1); + events.First().Name.Should().Be("Future Event"); + } + + #endregion + + #region Get Single Event - Positive Tests + + [Fact] + public async Task GetEvent_ReturnsPublishedEvent() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var eventEntity = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Published); + _context.Events.Add(eventEntity); + await _context.SaveChangesAsync(); + + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + // Act + var result = await _controller.GetEvent(eventEntity.Id); + + // Assert + var okResult = result.Result.Should().BeOfType().Subject; + var response = okResult.Value.Should().BeOfType().Subject; + response.Id.Should().Be(eventEntity.Id); + response.Name.Should().Be(eventEntity.Name); + } + + [Fact] + public async Task GetEvent_OrganizerCanViewOwnDraftEvent() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var draftEvent = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Draft); + _context.Events.Add(draftEvent); + await _context.SaveChangesAsync(); + + SetUserContext(organizer.Id, "Organizer"); + + // Act + var result = await _controller.GetEvent(draftEvent.Id); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Get Single Event - Negative Tests + + [Fact] + public async Task GetEvent_DraftEventNotVisibleToPublic() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var draftEvent = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Draft); + _context.Events.Add(draftEvent); + await _context.SaveChangesAsync(); + + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + // Act + var result = await _controller.GetEvent(draftEvent.Id); + + // Assert + result.Result.Should().BeOfType(); + } + + [Fact] + public async Task GetEvent_NonExistentEvent_ReturnsNotFound() + { + // Arrange + var nonExistentId = Guid.NewGuid(); + + _controller.ControllerContext = new ControllerContext + { + HttpContext = new DefaultHttpContext { User = new ClaimsPrincipal() } + }; + + // Act + var result = await _controller.GetEvent(nonExistentId); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Update Event - Positive Tests + + [Fact] + public async Task UpdateEvent_WithValidData_UpdatesEvent() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var eventEntity = TestDataFactory.CreateEvent(organizerId: organizer.Id, status: EventStatus.Draft); + _context.Events.Add(eventEntity); + await _context.SaveChangesAsync(); + + SetUserContext(organizer.Id, "Organizer"); + + var request = new UpdateEventRequest + { + Name = "Updated Event Name", + Description = "Updated description", + Status = EventStatus.Published + }; + + // Act + var result = await _controller.UpdateEvent(eventEntity.Id, request); + + // Assert + var okResult = result.Result.Should().BeOfType().Subject; + var response = okResult.Value.Should().BeOfType().Subject; + response.Name.Should().Be("Updated Event Name"); + response.Status.Should().Be("Published"); + } + + #endregion + + #region Update Event - Negative Tests + + [Fact] + public async Task UpdateEvent_NonOrganizer_ReturnsForbidden() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + var otherUser = TestDataFactory.CreateUser(email: "other@example.com", role: UserRole.Organizer); + _context.Users.AddRange(organizer, otherUser); + + var eventEntity = TestDataFactory.CreateEvent(organizerId: organizer.Id); + _context.Events.Add(eventEntity); + await _context.SaveChangesAsync(); + + SetUserContext(otherUser.Id, "Organizer"); + + var request = new UpdateEventRequest { Name = "Hacked Name" }; + + // Act + var result = await _controller.UpdateEvent(eventEntity.Id, request); + + // Assert + result.Result.Should().BeOfType(); + } + + [Fact] + public async Task UpdateEvent_NonExistentEvent_ReturnsNotFound() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + await _context.SaveChangesAsync(); + + SetUserContext(organizer.Id, "Organizer"); + + var request = new UpdateEventRequest { Name = "Updated Name" }; + + // Act + var result = await _controller.UpdateEvent(Guid.NewGuid(), request); + + // Assert + result.Result.Should().BeOfType(); + } + + #endregion + + #region Delete Event - Positive Tests + + [Fact] + public async Task DeleteEvent_OrganizerCanDeleteOwnEvent() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + + var eventEntity = TestDataFactory.CreateEvent(organizerId: organizer.Id); + _context.Events.Add(eventEntity); + await _context.SaveChangesAsync(); + + SetUserContext(organizer.Id, "Organizer"); + + // Act + var result = await _controller.DeleteEvent(eventEntity.Id); + + // Assert + result.Should().BeOfType(); + + var deletedEvent = await _context.Events.FindAsync(eventEntity.Id); + deletedEvent.Should().BeNull(); + } + + #endregion + + #region Delete Event - Negative Tests + + [Fact] + public async Task DeleteEvent_NonOrganizer_ReturnsForbidden() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + var otherUser = TestDataFactory.CreateUser(email: "other@example.com", role: UserRole.Organizer); + _context.Users.AddRange(organizer, otherUser); + + var eventEntity = TestDataFactory.CreateEvent(organizerId: organizer.Id); + _context.Events.Add(eventEntity); + await _context.SaveChangesAsync(); + + SetUserContext(otherUser.Id, "Organizer"); + + // Act + var result = await _controller.DeleteEvent(eventEntity.Id); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task DeleteEvent_NonExistentEvent_ReturnsNotFound() + { + // Arrange + var organizer = TestDataFactory.CreateUser(role: UserRole.Organizer); + _context.Users.Add(organizer); + await _context.SaveChangesAsync(); + + SetUserContext(organizer.Id, "Organizer"); + + // Act + var result = await _controller.DeleteEvent(Guid.NewGuid()); + + // Assert + result.Should().BeOfType(); + } + + #endregion +}