ef2fd39508
Backend CI / build-and-test (push) Successful in 48s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 5s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 24s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 14s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 2s
268 lines
9.6 KiB
C#
268 lines
9.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using UniVerse.Application.DTOs.Notifications;
|
|
using UniVerse.Application.DTOs.Users;
|
|
using UniVerse.Application.Interfaces;
|
|
using UniVerse.Domain.Entities;
|
|
using UniVerse.Domain.Enums;
|
|
using UniVerse.Domain.Exceptions;
|
|
using UniVerse.Infrastructure.Data;
|
|
using UniVerse.Infrastructure.Services;
|
|
using Xunit;
|
|
|
|
namespace UniVerse.Api.Tests.Users;
|
|
|
|
public class UserServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task GetStatsAsync_ReturnsLevelProgressThresholds()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
SeedLevelThresholds(db);
|
|
db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 120 });
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
var stats = await service.GetStatsAsync(1);
|
|
|
|
Assert.Equal(2, stats.Level);
|
|
Assert.Equal(100, stats.CurrentLevelXp);
|
|
Assert.Equal(300, stats.NextLevelXp);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_ReturnsNullNextLevelAtMaxConfiguredLevel()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
SeedLevelThresholds(db);
|
|
db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 350 });
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
var stats = await service.GetStatsAsync(1);
|
|
|
|
Assert.Equal(3, stats.Level);
|
|
Assert.Equal(300, stats.CurrentLevelXp);
|
|
Assert.Null(stats.NextLevelXp);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetStatsAsync_ReturnsEnrollmentSlotStateAndRules()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
SeedLevelThresholds(db);
|
|
var now = DateTime.UtcNow;
|
|
db.Users.Add(new User { Id = 1, Email = "student@test.local", Xp = 350 });
|
|
db.Courses.Add(new Course { Id = 1, Name = "Course" });
|
|
db.Lectures.AddRange(
|
|
Lecture(1, now.AddDays(1)),
|
|
Lecture(2, now.AddDays(2)),
|
|
Lecture(3, now.AddDays(-1)));
|
|
db.LectureEnrollments.AddRange(
|
|
new LectureEnrollment { LectureId = 1, UserId = 1 },
|
|
new LectureEnrollment { LectureId = 2, UserId = 1 },
|
|
new LectureEnrollment { LectureId = 3, UserId = 1 });
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
var stats = await service.GetStatsAsync(1);
|
|
|
|
Assert.Equal(3, stats.ActiveEnrollments);
|
|
Assert.Equal(5, stats.EnrollmentSlotLimit);
|
|
Assert.Equal(new[] { 1, 3, 4 }, stats.EnrollmentSlotRules.Select(rule => rule.Level));
|
|
Assert.Equal(new[] { 3, 5, 7 }, stats.EnrollmentSlotRules.Select(rule => rule.Slots));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetRolesAsync_DeduplicatesRolesAndCreatesProfiles()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
db.Users.Add(new User
|
|
{
|
|
Id = 1,
|
|
Email = "user@test.local",
|
|
Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }]
|
|
});
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
await service.SetRolesAsync(1, [UserRole.Teacher, UserRole.Teacher, UserRole.Student]);
|
|
|
|
var user = await db.Users
|
|
.Include(u => u.Roles)
|
|
.FirstAsync(u => u.Id == 1);
|
|
Assert.Equal(new[] { UserRole.Student, UserRole.Teacher }, user.Roles.Select(role => role.Role).OrderBy(role => role));
|
|
Assert.Equal(2, user.Roles.Count);
|
|
Assert.True(await db.StudentProfiles.AnyAsync(profile => profile.UserId == 1));
|
|
Assert.True(await db.TeacherProfiles.AnyAsync(profile => profile.UserId == 1));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetRolesAsync_RejectsEmptyRoleSet()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
db.Users.Add(new User
|
|
{
|
|
Id = 1,
|
|
Email = "user@test.local",
|
|
Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }]
|
|
});
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
await Assert.ThrowsAsync<ForbiddenException>(() => service.SetRolesAsync(1, []));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SetRolesAsync_PreservesExistingProfiles()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
db.Users.Add(new User
|
|
{
|
|
Id = 1,
|
|
Email = "user@test.local",
|
|
Roles = [new UserRoleAssignment { UserId = 1, Role = UserRole.Student }]
|
|
});
|
|
db.StudentProfiles.Add(new StudentProfile
|
|
{
|
|
Id = 10,
|
|
UserId = 1,
|
|
StudentId = "S-1"
|
|
});
|
|
db.TeacherProfiles.Add(new TeacherProfile
|
|
{
|
|
Id = 20,
|
|
UserId = 1,
|
|
Department = "Math"
|
|
});
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
await service.SetRolesAsync(1, [UserRole.Teacher]);
|
|
|
|
Assert.Equal(1, await db.StudentProfiles.CountAsync(profile => profile.UserId == 1));
|
|
Assert.Equal(1, await db.TeacherProfiles.CountAsync(profile => profile.UserId == 1));
|
|
Assert.Equal("S-1", (await db.StudentProfiles.SingleAsync(profile => profile.UserId == 1)).StudentId);
|
|
Assert.Equal("Math", (await db.TeacherProfiles.SingleAsync(profile => profile.UserId == 1)).Department);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_FiltersBySearchActiveAndExactSingleRole()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
SeedLevelThresholds(db);
|
|
db.Users.AddRange(
|
|
User(1, "anna@test.local", "Anna", true, 120, UserRole.Student),
|
|
User(2, "anna.teacher@test.local", "Anna Teacher", true, 120, UserRole.Teacher),
|
|
User(3, "anna.admin@test.local", "Anna Admin", true, 120, UserRole.Student, UserRole.Admin),
|
|
User(4, "inactive@test.local", "Anna Inactive", false, 120, UserRole.Student));
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
var result = await service.GetAllAsync(new UserFilterRequest(
|
|
Search: "anna",
|
|
Role: UserRole.Student,
|
|
IsActive: true,
|
|
Page: 1,
|
|
PageSize: 10));
|
|
|
|
var user = Assert.Single(result.Items);
|
|
Assert.Equal(1, user.Id);
|
|
Assert.Equal(2, user.Level);
|
|
Assert.Equal(1, result.TotalCount);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetAllAsync_ReturnsRequestedPageInCreatedAtDescendingOrder()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
SeedLevelThresholds(db);
|
|
db.Users.AddRange(
|
|
User(1, "old@test.local", "Old", true, 0, new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), UserRole.Student),
|
|
User(2, "middle@test.local", "Middle", true, 100, new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc), UserRole.Student),
|
|
User(3, "new@test.local", "New", true, 300, new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc), UserRole.Student));
|
|
await db.SaveChangesAsync();
|
|
var service = CreateService(db);
|
|
|
|
var result = await service.GetAllAsync(new UserFilterRequest(null, null, null, Page: 2, PageSize: 1));
|
|
|
|
Assert.Equal(3, result.TotalCount);
|
|
Assert.Equal(2, result.Page);
|
|
Assert.Equal(3, result.TotalPages);
|
|
Assert.Equal(2, Assert.Single(result.Items).Id);
|
|
}
|
|
|
|
private static AppDbContext CreateDbContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase($"UserServiceTests_{Guid.NewGuid()}")
|
|
.Options;
|
|
return new AppDbContext(options);
|
|
}
|
|
|
|
private static UserService CreateService(AppDbContext db)
|
|
{
|
|
var notifications = Substitute.For<INotificationService>();
|
|
notifications.CreateUserNotificationAsync(
|
|
Arg.Any<int>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<string>(),
|
|
Arg.Any<CancellationToken>())
|
|
.Returns(callInfo => new UserNotificationDto(1, callInfo.ArgAt<string>(1), callInfo.ArgAt<string>(2), callInfo.ArgAt<string>(3), false, DateTime.UtcNow));
|
|
notifications.SendAsync(Arg.Any<NotificationMessage>(), Arg.Any<CancellationToken>())
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var gamification = new GamificationService(db, notifications, NullLogger<GamificationService>.Instance);
|
|
return new UserService(db, gamification);
|
|
}
|
|
|
|
private static void SeedLevelThresholds(AppDbContext db)
|
|
{
|
|
db.LevelThresholds.AddRange(
|
|
new LevelThreshold { Level = 1, RequiredXp = 0 },
|
|
new LevelThreshold { Level = 2, RequiredXp = 100 },
|
|
new LevelThreshold { Level = 3, RequiredXp = 300 });
|
|
db.SaveChanges();
|
|
}
|
|
|
|
private static Lecture Lecture(int id, DateTime startsAt) => new()
|
|
{
|
|
Id = id,
|
|
CourseId = 1,
|
|
Title = $"Lecture {id}",
|
|
StartsAt = startsAt,
|
|
EndsAt = startsAt.AddHours(2),
|
|
IsOpen = true,
|
|
MaxEnrollments = 30
|
|
};
|
|
|
|
private static User User(
|
|
int id,
|
|
string email,
|
|
string displayName,
|
|
bool isActive,
|
|
int xp,
|
|
params UserRole[] roles) =>
|
|
User(id, email, displayName, isActive, xp, DateTime.UtcNow, roles);
|
|
|
|
private static User User(
|
|
int id,
|
|
string email,
|
|
string displayName,
|
|
bool isActive,
|
|
int xp,
|
|
DateTime createdAt,
|
|
params UserRole[] roles) => new()
|
|
{
|
|
Id = id,
|
|
Email = email,
|
|
DisplayName = displayName,
|
|
IsActive = isActive,
|
|
Xp = xp,
|
|
CreatedAt = createdAt,
|
|
Roles = roles.Select(role => new UserRoleAssignment { UserId = id, Role = role }).ToList()
|
|
};
|
|
}
|