using Microsoft.EntityFrameworkCore; using NSubstitute; using UniVerse.Application.DTOs.Reviews; using UniVerse.Application.DTOs.Common; 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.Reviews; public class ReviewServiceTests { [Fact] public async Task CreateAsync_EnqueuesReviewAnalysis() { await using var db = CreateDbContext(); var queue = Substitute.For(); var service = CreateService(db, queue); await SeedLectureAsync(db); var result = await service.CreateAsync(1, new CreateReviewRequest(1, ReviewRating.Like, "Great lecture")); await queue.Received(1).EnqueueAsync(result.Id, Arg.Any()); } [Fact] public async Task UpdateAsync_ResetsAnalysisAndEnqueuesReview() { await using var db = CreateDbContext(); var queue = Substitute.For(); var service = CreateService(db, queue); await SeedAnalyzedReviewAsync(db); await service.UpdateAsync(1, 1, new UpdateReviewRequest(ReviewRating.Neutral, "Updated text")); var review = await db.Reviews.SingleAsync(r => r.Id == 1); Assert.Equal(ReviewLlmStatus.Pending, review.LlmStatus); Assert.Null(review.Sentiment); Assert.Null(review.QualityScore); Assert.Null(review.IsInformative); Assert.Null(review.LlmTags); Assert.Null(review.LlmRawOutput); await queue.Received(1).EnqueueAsync(1, Arg.Any()); } [Fact] public async Task ReanalyzeAsync_ResetsAnalysisAndEnqueuesReview() { await using var db = CreateDbContext(); var queue = Substitute.For(); var service = CreateService(db, queue); await SeedAnalyzedReviewAsync(db); await service.ReanalyzeAsync(1); var review = await db.Reviews.SingleAsync(r => r.Id == 1); Assert.Equal(ReviewLlmStatus.Pending, review.LlmStatus); Assert.Null(review.Sentiment); Assert.Null(review.QualityScore); Assert.Null(review.IsInformative); Assert.Null(review.LlmTags); Assert.Null(review.LlmRawOutput); await queue.Received(1).EnqueueAsync(1, Arg.Any()); } [Fact] public async Task GetByLectureAsync_TeacherCannotReadAnotherTeachersReviews() { await using var db = CreateDbContext(); var service = CreateService(db, Substitute.For()); await SeedAnalyzedReviewAsync(db, teacherId: 2); await Assert.ThrowsAsync(() => service.GetByLectureAsync(1, new PaginationRequest(), currentUserId: 1)); } [Fact] public async Task GetByLectureAsync_AdminCanReadAnyLectureReviews() { await using var db = CreateDbContext(); var service = CreateService(db, Substitute.For()); await SeedAnalyzedReviewAsync(db, teacherId: 2); var result = await service.GetByLectureAsync(1, new PaginationRequest(), currentUserId: 1, isAdmin: true); Assert.Single(result.Items); } private static ReviewService CreateService(AppDbContext db, IReviewAnalysisQueue queue) { var gamification = Substitute.For(); gamification.CheckAndAwardAchievementsAsync(Arg.Any()).Returns(Task.CompletedTask); return new ReviewService(db, gamification, queue); } private static async Task SeedLectureAsync(AppDbContext db, int? teacherId = null) { db.Users.Add(new User { Id = 1, Email = "student@test.local", DisplayName = "Student" }); db.Courses.Add(new Course { Id = 1, Name = "Course" }); db.Lectures.Add(new Lecture { Id = 1, CourseId = 1, TeacherId = teacherId, Title = "Lecture", StartsAt = DateTime.UtcNow.AddDays(-1), EndsAt = DateTime.UtcNow.AddDays(-1).AddHours(2), IsOpen = true, MaxEnrollments = 30 }); await db.SaveChangesAsync(); } private static async Task SeedAnalyzedReviewAsync(AppDbContext db, int? teacherId = null) { await SeedLectureAsync(db, teacherId); db.Reviews.Add(new Review { Id = 1, LectureId = 1, UserId = 1, Rating = ReviewRating.Like, Text = "Original text", LlmStatus = ReviewLlmStatus.Analyzed, Sentiment = ReviewSentiment.Positive, QualityScore = 0.9, IsInformative = true, LlmTags = ["clear"], LlmRawOutput = "{\"quality_score\":0.9}" }); await db.SaveChangesAsync(); } private static AppDbContext CreateDbContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase($"ReviewServiceTests_{Guid.NewGuid()}") .Options; return new AppDbContext(options); } }