feat(ai): DB-only config + runtime provider selection via factory

This commit is contained in:
Chris Chen
2026-06-25 13:23:13 -07:00
parent ece9938bfb
commit 120240ad0c
8 changed files with 132 additions and 57 deletions
@@ -0,0 +1,69 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using ROLAC.API.Data;
using ROLAC.API.Data.Interceptors;
using ROLAC.API.Entities;
using ROLAC.API.Services.Ai;
using ROLAC.API.Services.Logging;
using Xunit;
namespace ROLAC.API.Tests.Services;
public class ExpenseAiServiceFactoryTests
{
// ChurchProfile is auditable, so the InMemory store rejects saves unless the
// required CreatedBy/UpdatedBy fields are populated. Wire the same audit
// interceptor the app uses so seeded entities save cleanly.
private static AppDbContext NewDb()
{
var httpContext = new DefaultHttpContext
{
User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, "test-user") })),
};
var httpContextAccessor = new Mock<IHttpContextAccessor>();
httpContextAccessor.Setup(accessor => accessor.HttpContext).Returns(httpContext);
return new AppDbContext(new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.ConfigureWarnings(warnings => warnings.Ignore(InMemoryEventId.TransactionIgnoredWarning))
.AddInterceptors(new AuditSaveChangesInterceptor(new CurrentUserAccessor(httpContextAccessor.Object)))
.Options);
}
private static ExpenseAiServiceFactory Build(AppDbContext db)
{
var cfg = new ChurchAiConfigProvider(db);
var claude = new ClaudeExpenseAiService(
new HttpClient(), cfg, db, NullLogger<ClaudeExpenseAiService>.Instance);
var gemini = new GeminiExpenseAiService(
new HttpClient(), cfg, db, NullLogger<GeminiExpenseAiService>.Instance);
return new ExpenseAiServiceFactory(cfg, claude, gemini);
}
[Fact]
public async Task Resolves_Claude_by_default()
{
using var db = NewDb();
db.ChurchProfiles.Add(new ChurchProfile { Name = "C", AiProvider = "Claude" });
await db.SaveChangesAsync();
var svc = await Build(db).ResolveAsync();
Assert.IsType<ClaudeExpenseAiService>(svc);
}
[Fact]
public async Task Resolves_Gemini_when_selected()
{
using var db = NewDb();
db.ChurchProfiles.Add(new ChurchProfile { Name = "C", AiProvider = "Gemini" });
await db.SaveChangesAsync();
var svc = await Build(db).ResolveAsync();
Assert.IsType<GeminiExpenseAiService>(svc);
}
}