using EchoHub.Core.Constants; using EchoHub.Core.DTOs; using EchoHub.Core.Models; using EchoHub.Server.Data; using EchoHub.Server.Services; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace EchoHub.Tests; /// /// authentication, profile reads, profile-update validation, and /// avatar. (Registration-gate behavior is covered by .) /// Runs against a real SQLite in-memory database with real BCrypt hashing. /// public sealed class UserServiceTests : IDisposable { private readonly SqliteConnection _connection; private readonly ServiceProvider _provider; private readonly UserService _service; public UserServiceTests() { _connection = new SqliteConnection("DataSource=:memory:"); _connection.Open(); var services = new ServiceCollection(); services.AddDbContext(o => o.UseSqlite(_connection)); _provider = services.BuildServiceProvider(); using var scope = _provider.CreateScope(); scope.ServiceProvider.GetRequiredService().Database.EnsureCreated(); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Server:Registration"] = "open" }) .Build(); _service = new UserService(_provider.GetRequiredService(), config); } public void Dispose() { _provider.Dispose(); _connection.Dispose(); } private async Task RegisterAsync(string username = "alice", string password = "password1") { var result = await _service.RegisterUserAsync(username, password); Assert.True(result.IsSuccess, result.ErrorMessage); return result.User!; } private EchoHubDbContext Db() => _provider.GetRequiredService() .CreateScope().ServiceProvider.GetRequiredService(); // ── Authentication ──────────────────────────────────────────────── [Fact] public async Task Authenticate_CorrectCredentials_Succeeds() { await RegisterAsync("alice", "password1"); var result = await _service.AuthenticateUserAsync("alice", "password1"); Assert.True(result.IsSuccess); Assert.Equal("alice", result.User!.Username); } [Fact] public async Task Authenticate_IsCaseInsensitiveOnUsername() { await RegisterAsync("alice", "password1"); var result = await _service.AuthenticateUserAsync("ALICE", "password1"); Assert.True(result.IsSuccess); } [Fact] public async Task Authenticate_WrongPassword_InvalidCredentials() { await RegisterAsync("alice", "password1"); var result = await _service.AuthenticateUserAsync("alice", "wrong"); Assert.False(result.IsSuccess); Assert.Equal(UserError.InvalidCredentials, result.Error); } [Fact] public async Task Authenticate_UnknownUser_InvalidCredentials() { var result = await _service.AuthenticateUserAsync("nobody", "password1"); Assert.False(result.IsSuccess); Assert.Equal(UserError.InvalidCredentials, result.Error); } [Fact] public async Task Authenticate_EmptyInput_ValidationFailed() { var result = await _service.AuthenticateUserAsync("", ""); Assert.False(result.IsSuccess); Assert.Equal(UserError.ValidationFailed, result.Error); } [Fact] public async Task Authenticate_BannedUser_ReturnsBanned() { var profile = await RegisterAsync("alice", "password1"); using (var scope = _provider.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var user = await db.Users.FindAsync(profile.Id); user!.IsBanned = true; await db.SaveChangesAsync(); } var result = await _service.AuthenticateUserAsync("alice", "password1"); Assert.False(result.IsSuccess); Assert.Equal(UserError.Banned, result.Error); } // ── Profile reads ───────────────────────────────────────────────── [Fact] public async Task GetUserProfile_KnownUser_ReturnsProfile() { await RegisterAsync("alice", "password1"); var profile = await _service.GetUserProfileAsync("alice"); Assert.NotNull(profile); Assert.Equal("alice", profile!.Username); } [Fact] public async Task GetUserProfile_UnknownUser_ReturnsNull() { Assert.Null(await _service.GetUserProfileAsync("ghost")); } [Fact] public async Task GetUserById_RoundTrips() { var profile = await RegisterAsync("alice", "password1"); var byId = await _service.GetUserByIdAsync(profile.Id); Assert.NotNull(byId); Assert.Equal("alice", byId!.Username); } [Fact] public async Task GetUserById_UnknownId_ReturnsNull() { Assert.Null(await _service.GetUserByIdAsync(Guid.NewGuid())); } // ── Profile updates ─────────────────────────────────────────────── [Fact] public async Task UpdateProfile_ValidFields_Persisted() { var profile = await RegisterAsync("alice", "password1"); var result = await _service.UpdateProfileAsync(profile.Id, "Alice A", "hi there", "#FF5500"); Assert.True(result.IsSuccess); Assert.Equal("Alice A", result.User!.DisplayName); Assert.Equal("hi there", result.User.Bio); Assert.Equal("#FF5500", result.User.NicknameColor); } [Fact] public async Task UpdateProfile_InvalidHexColor_Rejected() { var profile = await RegisterAsync("alice", "password1"); var result = await _service.UpdateProfileAsync(profile.Id, null, null, "red"); Assert.False(result.IsSuccess); Assert.Equal(UserError.ValidationFailed, result.Error); } [Fact] public async Task UpdateProfile_EmptyColor_ClearsIt() { var profile = await RegisterAsync("alice", "password1"); await _service.UpdateProfileAsync(profile.Id, null, null, "#FF5500"); var result = await _service.UpdateProfileAsync(profile.Id, null, null, ""); Assert.True(result.IsSuccess); Assert.Null(result.User!.NicknameColor); } [Fact] public async Task UpdateProfile_DisplayNameTooLong_Rejected() { var profile = await RegisterAsync("alice", "password1"); var result = await _service.UpdateProfileAsync( profile.Id, new string('x', ValidationConstants.MaxDisplayNameLength + 1), null, null); Assert.False(result.IsSuccess); Assert.Equal(UserError.ValidationFailed, result.Error); } [Fact] public async Task UpdateProfile_BioTooLong_Rejected() { var profile = await RegisterAsync("alice", "password1"); var result = await _service.UpdateProfileAsync( profile.Id, null, new string('x', ValidationConstants.MaxBioLength + 1), null); Assert.False(result.IsSuccess); Assert.Equal(UserError.ValidationFailed, result.Error); } [Fact] public async Task UpdateProfile_UnknownUser_NotFound() { var result = await _service.UpdateProfileAsync(Guid.NewGuid(), "x", null, null); Assert.False(result.IsSuccess); Assert.Equal(UserError.NotFound, result.Error); } // ── Avatar ──────────────────────────────────────────────────────── [Fact] public async Task SetAvatar_Persisted() { var profile = await RegisterAsync("alice", "password1"); var result = await _service.SetAvatarAsync(profile.Id, "{F:FF0000}art"); Assert.True(result.IsSuccess); var stored = await Db().Users.FindAsync(profile.Id); Assert.Equal("{F:FF0000}art", stored!.AvatarAscii); } [Fact] public async Task SetAvatar_UnknownUser_NotFound() { var result = await _service.SetAvatarAsync(Guid.NewGuid(), "art"); Assert.False(result.IsSuccess); Assert.Equal(UserError.NotFound, result.Error); } }