8ac593d36f
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
185 lines
6.7 KiB
C#
185 lines
6.7 KiB
C#
using System.Net;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NSubstitute;
|
|
using UniVerse.Application.DTOs.Reviews;
|
|
using UniVerse.Application.Interfaces;
|
|
using UniVerse.Application.Prompts;
|
|
using UniVerse.Domain.Exceptions;
|
|
using UniVerse.Infrastructure.Data;
|
|
using UniVerse.Infrastructure.ExternalServices;
|
|
using UniVerse.Infrastructure.Services;
|
|
using Xunit;
|
|
|
|
namespace UniVerse.Api.Tests.Reviews;
|
|
|
|
public class ReviewPromptServiceTests
|
|
{
|
|
[Fact]
|
|
public async Task GetAsync_ReturnsDefaultPrompt_WhenSettingDoesNotExist()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
var service = new ReviewPromptService(db);
|
|
|
|
var result = await service.GetAsync();
|
|
|
|
Assert.Equal(ReviewPromptTemplate.Default, result.Prompt);
|
|
Assert.Null(result.UpdatedAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateAsync_UpsertsSingletonPrompt()
|
|
{
|
|
await using var db = CreateDbContext();
|
|
var service = new ReviewPromptService(db);
|
|
|
|
await service.UpdateAsync(new UpdateReviewPromptRequest("First {lectureContext} {reviewText}"));
|
|
var result = await service.UpdateAsync(new UpdateReviewPromptRequest("Second {lectureContext} {reviewText}"));
|
|
|
|
Assert.Equal("Second {lectureContext} {reviewText}", result.Prompt);
|
|
Assert.NotNull(result.UpdatedAt);
|
|
Assert.Equal(1, await db.ReviewPromptSettings.CountAsync());
|
|
Assert.Equal("Second {lectureContext} {reviewText}", (await service.GetAsync()).Prompt);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
[InlineData("Prompt without placeholders")]
|
|
[InlineData("Only lecture {lectureContext}")]
|
|
[InlineData("Only review {reviewText}")]
|
|
public async Task UpdateAsync_RejectsInvalidPrompt(string prompt)
|
|
{
|
|
await using var db = CreateDbContext();
|
|
var service = new ReviewPromptService(db);
|
|
|
|
await Assert.ThrowsAsync<BadRequestException>(() =>
|
|
service.UpdateAsync(new UpdateReviewPromptRequest(prompt)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnalyzeReviewAsync_RendersCustomPrompt()
|
|
{
|
|
var handler = new CapturingHandler();
|
|
var http = new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri("https://llm.test/")
|
|
};
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Llm:Model"] = "test-model",
|
|
["Llm:ApiKey"] = "test-key"
|
|
})
|
|
.Build();
|
|
var promptService = Substitute.For<IReviewPromptService>();
|
|
promptService.GetAsync().Returns(new ReviewPromptDto(
|
|
"Custom prompt. Context: {lectureContext}. Text: {reviewText}",
|
|
DateTime.UtcNow));
|
|
var client = new LlmClient(http, config, promptService, NullLogger<LlmClient>.Instance);
|
|
|
|
await client.AnalyzeReviewAsync("Very useful review", "Lecture: Algebra");
|
|
|
|
Assert.NotNull(handler.RequestBody);
|
|
using var requestJson = JsonDocument.Parse(handler.RequestBody!);
|
|
var content = requestJson.RootElement
|
|
.GetProperty("messages")[0]
|
|
.GetProperty("content")
|
|
.GetString();
|
|
|
|
Assert.Contains("Custom prompt", content);
|
|
Assert.Contains("Lecture: Algebra", content);
|
|
Assert.Contains("Very useful review", content);
|
|
Assert.DoesNotContain(ReviewPromptTemplate.LectureContextPlaceholder, content);
|
|
Assert.DoesNotContain(ReviewPromptTemplate.ReviewTextPlaceholder, content);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task AnalyzeReviewAsync_ParsesSnakeCaseJsonFromFencedResponse()
|
|
{
|
|
var handler = new CapturingHandler("""
|
|
```json
|
|
{"quality_score":0.82,"sentiment":"Положительный","tags":["lecture structure","practical examples"],"is_informative":true}
|
|
```
|
|
""");
|
|
var http = new HttpClient(handler)
|
|
{
|
|
BaseAddress = new Uri("https://llm.test/")
|
|
};
|
|
var config = new ConfigurationBuilder()
|
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
|
{
|
|
["Llm:Model"] = "test-model",
|
|
["Llm:ApiKey"] = "test-key"
|
|
})
|
|
.Build();
|
|
var promptService = Substitute.For<IReviewPromptService>();
|
|
promptService.GetAsync().Returns(new ReviewPromptDto(ReviewPromptTemplate.Default, null));
|
|
var client = new LlmClient(http, config, promptService, NullLogger<LlmClient>.Instance);
|
|
|
|
var result = await client.AnalyzeReviewAsync("Very useful review", "Lecture: Algebra");
|
|
|
|
Assert.Equal(0.82, result.QualityScore);
|
|
Assert.Equal("Положительный", result.Sentiment);
|
|
Assert.Equal(["lecture structure", "practical examples"], result.Tags);
|
|
Assert.True(result.IsInformative);
|
|
Assert.Equal("""
|
|
```json
|
|
{"quality_score":0.82,"sentiment":"Положительный","tags":["lecture structure","practical examples"],"is_informative":true}
|
|
```
|
|
""", result.RawOutput);
|
|
}
|
|
|
|
private static AppDbContext CreateDbContext()
|
|
{
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseInMemoryDatabase($"ReviewPromptServiceTests_{Guid.NewGuid()}")
|
|
.Options;
|
|
return new AppDbContext(options);
|
|
}
|
|
|
|
private sealed class CapturingHandler : HttpMessageHandler
|
|
{
|
|
private readonly string _analysisContent;
|
|
|
|
public CapturingHandler(string? analysisContent = null)
|
|
{
|
|
_analysisContent = analysisContent ??
|
|
"{\"quality_score\":0.8,\"sentiment\":\"Positive\",\"tags\":[\"practice\"],\"is_informative\":true}";
|
|
}
|
|
|
|
public string? RequestBody { get; private set; }
|
|
|
|
protected override async Task<HttpResponseMessage> SendAsync(
|
|
HttpRequestMessage request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
RequestBody = request.Content is null
|
|
? null
|
|
: await request.Content.ReadAsStringAsync(cancellationToken);
|
|
|
|
var responsePayload = JsonSerializer.Serialize(new
|
|
{
|
|
choices = new[]
|
|
{
|
|
new
|
|
{
|
|
message = new
|
|
{
|
|
content = _analysisContent
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return new HttpResponseMessage(HttpStatusCode.OK)
|
|
{
|
|
Content = new StringContent(responsePayload, Encoding.UTF8, "application/json")
|
|
};
|
|
}
|
|
}
|
|
}
|