mirror of
https://github.com/RedWizardsLab/EchoHub.git
synced 2026-09-04 16:46:08 +02:00
260 lines
8.5 KiB
C#
260 lines
8.5 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// <see cref="UserService"/> authentication, profile reads, profile-update validation, and
|
|
/// avatar. (Registration-gate behavior is covered by <see cref="UserServiceRegistrationTests"/>.)
|
|
/// Runs against a real SQLite in-memory database with real BCrypt hashing.
|
|
/// </summary>
|
|
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<EchoHubDbContext>(o => o.UseSqlite(_connection));
|
|
_provider = services.BuildServiceProvider();
|
|
|
|
using var scope = _provider.CreateScope();
|
|
scope.ServiceProvider.GetRequiredService<EchoHubDbContext>().Database.EnsureCreated();
|
|
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?> { ["Server:Registration"] = "open" })
|
|
.Build();
|
|
_service = new UserService(_provider.GetRequiredService<IServiceScopeFactory>(), config);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_provider.Dispose();
|
|
_connection.Dispose();
|
|
}
|
|
|
|
private async Task<UserProfileDto> 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<IServiceScopeFactory>()
|
|
.CreateScope().ServiceProvider.GetRequiredService<EchoHubDbContext>();
|
|
|
|
// ── 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<EchoHubDbContext>();
|
|
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);
|
|
}
|
|
}
|