using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class StrategySnapshotOperationTests { [Fact] public async Task Producer_returns_202_and_duplicate_click_reuses_the_operation() { await using var fixture = await Fixture.CreateAsync(); await fixture.SeedAsync(); await using var scope = fixture.Provider.CreateAsyncScope(); using var owner = scope.ServiceProvider.GetRequiredService().UseBackgroundUser("user-1"); var controller = Controller(scope); var first = Assert.IsType(await controller.Enqueue(1, new StrategySnapshotRequest(null), default)); var duplicate = Assert.IsType(await controller.Enqueue(1, new StrategySnapshotRequest(null), default)); var firstBody = Assert.IsType(first.Value); var duplicateBody = Assert.IsType(duplicate.Value); Assert.True(firstBody.Created); Assert.False(duplicateBody.Created); Assert.Equal(firstBody.Operation.Id, duplicateBody.Operation.Id); Assert.Equal(OperationStatuses.Queued, firstBody.Operation.Status); Assert.Single(await scope.ServiceProvider.GetRequiredService().UserOperations.ToListAsync()); } [Fact] public async Task Worker_persists_one_result_and_provider_metadata_then_GET_is_read_only() { await using var fixture = await Fixture.CreateAsync(); await fixture.SeedAsync(); await fixture.EnqueueAsync(); Assert.True(await fixture.Provider.GetRequiredService().RunOnceAsync(default)); Assert.False(await fixture.Provider.GetRequiredService().RunOnceAsync(default)); await using var scope = fixture.Provider.CreateAsyncScope(); using var owner = scope.ServiceProvider.GetRequiredService().UseBackgroundUser("user-1"); var controller = Controller(scope); var result = Assert.IsType((await controller.Get(1, null, default)).Result); Assert.Equal("Lead with delivery evidence.", Assert.IsType(result.Value).StrategicSummary); var db = scope.ServiceProvider.GetRequiredService(); Assert.Single(await db.AiWorkspaceNotes.ToListAsync()); var operation = Assert.Single(await db.UserOperations.ToListAsync()); Assert.Equal(OperationStatuses.Succeeded, operation.Status); Assert.Equal("ollama", operation.Provider); Assert.Equal("qwen-test", operation.Model); Assert.Equal(1, fixture.GenerationCalls); var usage = Assert.Single(await db.AiUsageRecords.ToListAsync()); Assert.Equal("strategy.snapshot", usage.TaskType); Assert.True(usage.InputCharacterCount > 0); Assert.Equal(Fixture.ValidResponse.Length, usage.OutputCharacterCount); Assert.Equal((usage.InputCharacterCount + usage.OutputCharacterCount + 3) / 4, usage.EstimatedTokenCount); Assert.True(usage.EstimatedTokenCount < AiUsageMeter.ReservationFor(StrategySnapshotService.TaskType).EstimatedTokens); } [Fact] public async Task Invalid_provider_shape_is_retryable_and_does_not_publish_partial_output() { await using var fixture = await Fixture.CreateAsync("not-json"); await fixture.SeedAsync(); await fixture.EnqueueAsync(); Assert.True(await fixture.Provider.GetRequiredService().RunOnceAsync(default)); await using var scope = fixture.Provider.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var operation = await db.UserOperations.IgnoreQueryFilters().SingleAsync(); Assert.Equal(OperationStatuses.WaitingForRetry, operation.Status); Assert.Equal("invalid_provider_response", operation.FailureCategory); Assert.Empty(await db.AiWorkspaceNotes.IgnoreQueryFilters().ToListAsync()); } [Fact] public async Task Latest_operation_and_result_are_tenant_scoped() { await using var fixture = await Fixture.CreateAsync(); await fixture.SeedAsync(); await fixture.SeedUserAsync("user-2", pro: true); await fixture.EnqueueAsync(); await using var scope = fixture.Provider.CreateAsyncScope(); using var owner = scope.ServiceProvider.GetRequiredService().UseBackgroundUser("user-2"); var controller = Controller(scope); Assert.IsType((await controller.LatestOperation(1, null, default)).Result); Assert.IsType((await controller.Get(1, null, default)).Result); } private static StrategySnapshotController Controller(AsyncServiceScope scope) { var controller = ActivatorUtilities.CreateInstance(scope.ServiceProvider); controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }; return controller; } private sealed class Fixture : IAsyncDisposable { internal const string ValidResponse = """{"strategicSummary":"Lead with delivery evidence.","cvBulletIdeas":["Built APIs"],"proofPointsToLeadWith":["Led delivery"],"coverLetterAngles":["Platform relevance"]}"""; private readonly SqliteConnection _connection; private readonly Mock _summarizer; public ServiceProvider Provider { get; } public int GenerationCalls { get; private set; } private Fixture(SqliteConnection connection, ServiceProvider provider, Mock summarizer) { _connection = connection; Provider = provider; _summarizer = summarizer; } public static async Task CreateAsync(string response = ValidResponse) { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); Fixture? fixture = null; var summarizer = new Mock(); summarizer.Setup(item => item.GenerateSectionWithMetadataAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(() => { fixture!.GenerationCalls++; return new AiGenerationResult(response, "ollama", "qwen-test", RouteReason: "local_primary"); }); var configuration = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["AiQueue:HeartbeatSeconds"] = "5", ["AiQueue:MaxAttempts"] = "3", ["Ai:ExternalProcessingEnabled"] = "false", }).Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddLogging(); services.AddHttpContextAccessor(); services.AddScoped(); services.AddScoped(provider => provider.GetRequiredService()); services.AddDbContext((_, options) => options.UseSqlite(connection)); services.AddIdentityCore().AddRoles().AddEntityFrameworkStores(); services.AddSingleton(TimeProvider.System); services.AddSingleton(); services.AddSingleton(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(summarizer.Object); var provider = services.BuildServiceProvider(); fixture = new Fixture(connection, provider, summarizer); await using var scope = provider.CreateAsyncScope(); await scope.ServiceProvider.GetRequiredService().Database.EnsureCreatedAsync(); return fixture; } public async Task SeedAsync() { await SeedUserAsync("user-1", pro: true); await using var scope = Provider.CreateAsyncScope(); using var owner = scope.ServiceProvider.GetRequiredService().UseBackgroundUser("user-1"); var db = scope.ServiceProvider.GetRequiredService(); var company = new Company { Id = 1, Name = "Acme", OwnerUserId = "user-1" }; db.Companies.Add(company); db.JobApplications.Add(new JobApplication { Id = 1, CompanyId = 1, Company = company, JobTitle = "Backend Developer", Description = "Needs .NET and SQL.", OwnerUserId = "user-1" }); await db.SaveChangesAsync(); } public async Task SeedUserAsync(string userId, bool pro) { await using var scope = Provider.CreateAsyncScope(); var roles = scope.ServiceProvider.GetRequiredService>(); if (pro && !await roles.RoleExistsAsync("Premium")) Assert.True((await roles.CreateAsync(new IdentityRole("Premium"))).Succeeded); var users = scope.ServiceProvider.GetRequiredService>(); var user = new ApplicationUser { Id = userId, UserName = $"{userId}@example.test", Email = $"{userId}@example.test", EmailConfirmed = true, AiEnabled = true, ProfileCvText = "Built .NET APIs and led delivery." }; Assert.True((await users.CreateAsync(user)).Succeeded); if (pro) Assert.True((await users.AddToRoleAsync(user, "Premium")).Succeeded); } public async Task EnqueueAsync() { await using var scope = Provider.CreateAsyncScope(); using var owner = scope.ServiceProvider.GetRequiredService().UseBackgroundUser("user-1"); var controller = Controller(scope); Assert.IsType(await controller.Enqueue(1, new StrategySnapshotRequest(null), default)); } public async ValueTask DisposeAsync() { _summarizer.Reset(); await Provider.DisposeAsync(); await _connection.DisposeAsync(); } } }