PROFILEPHOTO: profilePhotoUrl computed field on GerbilDto
GET /gerbils/{id}: loads first photo by sortOrder (2 queries, no N+1).
GET /gerbils list: batch-loads first photos for the page in one IN query.
profilePhotoUrl = /photos/files/{fileName} or null. No migration.
3 new tests: no-photo=null, sortOrder priority, list batch.
This commit is contained in:
83
GerbilManager.Tests/GerbilProfilePhotoTests.cs
Normal file
83
GerbilManager.Tests/GerbilProfilePhotoTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace GerbilManager.Tests;
|
||||
|
||||
/// <summary>profilePhotoUrl: computed field on GerbilDto — first photo by sortOrder, or null.</summary>
|
||||
public class GerbilProfilePhotoTests : IClassFixture<ApiFactory>
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly ApiFactory _factory;
|
||||
|
||||
public GerbilProfilePhotoTests(ApiFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
_client = factory.CreateClient();
|
||||
}
|
||||
|
||||
private static Guid ExtractId(string json) =>
|
||||
Guid.Parse(Regex.Match(json, "\"id\":\"([^\"]+)\"").Groups[1].Value);
|
||||
|
||||
private static string? GetStr(JsonElement el, string prop) =>
|
||||
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String
|
||||
? v.GetString() : null;
|
||||
|
||||
private async Task<Guid> CreateGerbil(string name)
|
||||
{
|
||||
var resp = await _client.PostAsync("/gerbils", JsonContent.Create(new { name, gender = "female" }));
|
||||
Assert.Equal(HttpStatusCode.Created, resp.StatusCode);
|
||||
return ExtractId(await resp.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private void InsertPhotos(Guid gerbilId, params (string fileName, int sortOrder)[] photos)
|
||||
{
|
||||
using var scope = _factory.Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<ApplicationContext>();
|
||||
foreach (var (fn, so) in photos)
|
||||
db.GerbilPhotos.Add(new GerbilPhoto
|
||||
{
|
||||
Id = Guid.NewGuid(), GerbilId = gerbilId,
|
||||
FileName = fn, SortOrder = so, CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetById_without_photos_returns_null_profilePhotoUrl()
|
||||
{
|
||||
var id = await CreateGerbil("NoPhoto");
|
||||
var json = await _client.GetStringAsync($"/gerbils/{id}");
|
||||
var el = JsonDocument.Parse(json).RootElement;
|
||||
Assert.True(el.TryGetProperty("profilePhotoUrl", out var v));
|
||||
Assert.Equal(JsonValueKind.Null, v.ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetById_with_photos_returns_lowest_sortOrder_url()
|
||||
{
|
||||
var id = await CreateGerbil("PhotoGerbil");
|
||||
InsertPhotos(id, ("second.jpg", 1), ("first.jpg", 0));
|
||||
|
||||
var json = await _client.GetStringAsync($"/gerbils/{id}");
|
||||
var el = JsonDocument.Parse(json).RootElement;
|
||||
Assert.Equal("/photos/files/first.jpg", GetStr(el, "profilePhotoUrl"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetList_returns_profilePhotoUrl_without_n_plus_1()
|
||||
{
|
||||
var id = await CreateGerbil("ListPhoto");
|
||||
InsertPhotos(id, ("list-photo.jpg", 0));
|
||||
|
||||
var json = await _client.GetStringAsync("/gerbils");
|
||||
var root = JsonDocument.Parse(json).RootElement;
|
||||
var items = root.GetProperty("items").EnumerateArray().ToList();
|
||||
var gerbil = items.FirstOrDefault(x => GetStr(x, "id") == id.ToString());
|
||||
Assert.NotEqual(default, gerbil);
|
||||
Assert.Equal("/photos/files/list-photo.jpg", GetStr(gerbil, "profilePhotoUrl"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user