2026-04-05 22:16:44 +02:00
|
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
|
using Moq;
|
|
|
|
|
using FluentAssertions;
|
|
|
|
|
using RacePlannerApi.Controllers;
|
|
|
|
|
using RacePlannerApi.Data;
|
|
|
|
|
using RacePlannerApi.DTOs;
|
|
|
|
|
using RacePlannerApi.Models;
|
|
|
|
|
using RacePlannerApi.Services;
|
|
|
|
|
using backend.Tests.Utilities;
|
|
|
|
|
|
|
|
|
|
namespace backend.Tests.Controllers;
|
|
|
|
|
|
|
|
|
|
public class AuthControllerTests : IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly RacePlannerDbContext _context;
|
2026-04-05 22:41:58 +02:00
|
|
|
private readonly JwtTokenService _jwtService;
|
2026-04-05 22:16:44 +02:00
|
|
|
private readonly AuthController _controller;
|
|
|
|
|
|
|
|
|
|
public AuthControllerTests()
|
|
|
|
|
{
|
|
|
|
|
var options = new DbContextOptionsBuilder<RacePlannerDbContext>()
|
|
|
|
|
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
|
|
|
|
|
.Options;
|
|
|
|
|
|
|
|
|
|
_context = new RacePlannerDbContext(options);
|
2026-04-05 22:41:58 +02:00
|
|
|
|
|
|
|
|
// Create real JwtTokenService with test configuration
|
2026-04-05 22:16:44 +02:00
|
|
|
var mockConfiguration = new Mock<Microsoft.Extensions.Configuration.IConfiguration>();
|
|
|
|
|
mockConfiguration.Setup(x => x["Jwt:Key"]).Returns("test-secret-key-here-minimum-32-characters-long");
|
|
|
|
|
mockConfiguration.Setup(x => x["Jwt:Issuer"]).Returns("TestIssuer");
|
|
|
|
|
mockConfiguration.Setup(x => x["Jwt:Audience"]).Returns("TestAudience");
|
2026-04-05 22:41:58 +02:00
|
|
|
|
|
|
|
|
_jwtService = new JwtTokenService(mockConfiguration.Object);
|
|
|
|
|
|
|
|
|
|
_controller = new AuthController(_context, _jwtService);
|
2026-04-05 22:16:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
_context.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Registration - Positive Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_WithValidData_CreatesNewUser()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "newuser@example.com",
|
|
|
|
|
Password = "SecurePass123!",
|
|
|
|
|
Name = "New User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
|
|
|
|
|
var response = okResult.Value.Should().BeOfType<AuthResponse>().Subject;
|
2026-04-05 22:41:58 +02:00
|
|
|
response.Token.Should().NotBeNullOrEmpty();
|
2026-04-05 22:16:44 +02:00
|
|
|
response.User.Email.Should().Be(request.Email);
|
|
|
|
|
response.User.Name.Should().Be(request.Name);
|
2026-04-05 22:41:58 +02:00
|
|
|
|
2026-04-05 22:16:44 +02:00
|
|
|
// Verify user was created in database
|
|
|
|
|
var userInDb = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
|
|
|
|
userInDb.Should().NotBeNull();
|
|
|
|
|
userInDb!.Name.Should().Be(request.Name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_WithOrganizerRole_CreatesOrganizerUser()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "organizer@example.com",
|
|
|
|
|
Password = "SecurePass123!",
|
|
|
|
|
Name = "Event Organizer",
|
|
|
|
|
Role = UserRole.Organizer
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
|
|
|
|
|
var response = okResult.Value.Should().BeOfType<AuthResponse>().Subject;
|
|
|
|
|
response.User.Role.Should().Be("Organizer");
|
|
|
|
|
|
|
|
|
|
var userInDb = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
|
|
|
|
userInDb!.Role.Should().Be(UserRole.Organizer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Registration - Negative Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_WithDuplicateEmail_ReturnsConflict()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var existingUser = TestDataFactory.CreateUser(email: "duplicate@example.com");
|
|
|
|
|
_context.Users.Add(existingUser);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "duplicate@example.com",
|
|
|
|
|
Password = "SecurePass123!",
|
|
|
|
|
Name = "Duplicate User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var conflictResult = result.Result.Should().BeOfType<ConflictObjectResult>().Subject;
|
|
|
|
|
conflictResult.Value.Should().BeEquivalentTo(new { error = "Email already registered" });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 22:41:58 +02:00
|
|
|
[Fact(Skip = "Password validation not yet implemented in controller")]
|
2026-04-05 22:16:44 +02:00
|
|
|
public async Task Register_WithWeakPassword_ReturnsValidationError()
|
|
|
|
|
{
|
|
|
|
|
// Note: In a real implementation, you'd add validation attributes
|
|
|
|
|
// This test assumes validation is handled by the controller or model
|
|
|
|
|
// For now, this documents the expected behavior
|
2026-04-05 22:41:58 +02:00
|
|
|
|
2026-04-05 22:16:44 +02:00
|
|
|
// Arrange
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "weakpass@example.com",
|
|
|
|
|
Password = "123", // Weak password
|
|
|
|
|
Name = "Weak Password User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
// If validation is implemented, this should return BadRequest
|
|
|
|
|
// For now, we assume password validation is not yet implemented
|
|
|
|
|
var result = await _controller.Register(request);
|
2026-04-05 22:41:58 +02:00
|
|
|
|
2026-04-05 22:16:44 +02:00
|
|
|
// Assert - this will pass if no validation, fail if validation exists
|
|
|
|
|
// In production, you'd check for BadRequestObjectResult
|
|
|
|
|
result.Result.Should().NotBeOfType<OkObjectResult>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_WithMismatchedPasswordConfirmation_ReturnsValidationError()
|
|
|
|
|
{
|
|
|
|
|
// Note: This assumes RegisterRequest has ConfirmPassword field
|
|
|
|
|
// If not present in current implementation, this test documents expected behavior
|
|
|
|
|
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "mismatch@example.com",
|
|
|
|
|
Password = "SecurePass123!",
|
|
|
|
|
Name = "Mismatch User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert - without ConfirmPassword, this will create user
|
|
|
|
|
// In full implementation, should validate password match
|
|
|
|
|
if (request.GetType().GetProperty("ConfirmPassword") != null)
|
|
|
|
|
{
|
|
|
|
|
result.Result.Should().NotBeOfType<OkObjectResult>();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_WithInvalidEmailFormat_ReturnsValidationError()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "invalid-email-format",
|
|
|
|
|
Password = "SecurePass123!",
|
|
|
|
|
Name = "Invalid Email User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
// Email validation is handled by [EmailAddress] attribute
|
|
|
|
|
// If model state validation is added to controller
|
|
|
|
|
var result = await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
// Without explicit model validation check in controller, this might pass
|
|
|
|
|
// In full implementation, controller should check ModelState.IsValid
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Login - Positive Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_WithCorrectCredentials_ReturnsToken()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var password = "CorrectPass123!";
|
|
|
|
|
var user = TestDataFactory.CreateUser(
|
|
|
|
|
email: "login@example.com",
|
|
|
|
|
password: password);
|
|
|
|
|
_context.Users.Add(user);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "login@example.com",
|
|
|
|
|
Password = password
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
|
|
|
|
|
var response = okResult.Value.Should().BeOfType<AuthResponse>().Subject;
|
2026-04-05 22:41:58 +02:00
|
|
|
response.Token.Should().NotBeNullOrEmpty();
|
2026-04-05 22:16:44 +02:00
|
|
|
response.User.Email.Should().Be(request.Email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_AsOrganizer_ReturnsOrganizerToken()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var password = "OrganizerPass123!";
|
|
|
|
|
var user = TestDataFactory.CreateUser(
|
|
|
|
|
email: "organizer@example.com",
|
|
|
|
|
password: password,
|
|
|
|
|
role: UserRole.Organizer);
|
|
|
|
|
_context.Users.Add(user);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "organizer@example.com",
|
|
|
|
|
Password = password
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var okResult = result.Result.Should().BeOfType<OkObjectResult>().Subject;
|
|
|
|
|
var response = okResult.Value.Should().BeOfType<AuthResponse>().Subject;
|
|
|
|
|
response.User.Role.Should().Be("Organizer");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Login - Negative Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_WithIncorrectPassword_ReturnsUnauthorized()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var user = TestDataFactory.CreateUser(
|
|
|
|
|
email: "wrongpass@example.com",
|
|
|
|
|
password: "CorrectPass123!");
|
|
|
|
|
_context.Users.Add(user);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "wrongpass@example.com",
|
|
|
|
|
Password = "WrongPass123!"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var unauthorizedResult = result.Result.Should().BeOfType<UnauthorizedObjectResult>().Subject;
|
|
|
|
|
unauthorizedResult.Value.Should().BeEquivalentTo(new { error = "Invalid credentials" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_WithNonExistentEmail_ReturnsUnauthorized()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "nonexistent@example.com",
|
|
|
|
|
Password = "SomePass123!"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var unauthorizedResult = result.Result.Should().BeOfType<UnauthorizedObjectResult>().Subject;
|
|
|
|
|
unauthorizedResult.Value.Should().BeEquivalentTo(new { error = "Invalid credentials" });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_WithEmptyCredentials_ReturnsValidationError()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "",
|
|
|
|
|
Password = ""
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
// Without validation, this might succeed or fail
|
|
|
|
|
// In full implementation, should validate required fields
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Security Tests
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Register_PasswordIsHashed_NotStoredPlaintext()
|
|
|
|
|
{
|
|
|
|
|
// Arrange
|
|
|
|
|
var plainPassword = "SecurePass123!";
|
|
|
|
|
var request = new RegisterRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "hashed@example.com",
|
|
|
|
|
Password = plainPassword,
|
|
|
|
|
Name = "Hashed Password User",
|
|
|
|
|
Role = UserRole.Participant
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
await _controller.Register(request);
|
|
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
|
var userInDb = await _context.Users.FirstOrDefaultAsync(u => u.Email == request.Email);
|
|
|
|
|
userInDb.Should().NotBeNull();
|
|
|
|
|
userInDb!.PasswordHash.Should().NotBe(plainPassword);
|
|
|
|
|
userInDb.PasswordHash.Should().StartWith("$2"); // BCrypt hash format
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Fact]
|
|
|
|
|
public async Task Login_CaseInsensitiveEmail_MatchesUser()
|
|
|
|
|
{
|
|
|
|
|
// Arrange - Note: This depends on database collation
|
|
|
|
|
// PostgreSQL is case-sensitive by default for text comparison
|
|
|
|
|
var user = TestDataFactory.CreateUser(
|
|
|
|
|
email: "CaseSensitive@Example.com",
|
|
|
|
|
password: "Pass123!");
|
|
|
|
|
_context.Users.Add(user);
|
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
|
|
|
|
|
|
var request = new LoginRequest
|
|
|
|
|
{
|
|
|
|
|
Email = "casesensitive@example.com", // Different case
|
|
|
|
|
Password = "Pass123!"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Act
|
|
|
|
|
var result = await _controller.Login(request);
|
|
|
|
|
|
|
|
|
|
// Assert - behavior depends on implementation
|
|
|
|
|
// In PostgreSQL, this might not match due to case sensitivity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
}
|