using System.Security.Claims; using JobTrackerApi.Controllers; using JobTrackerApi.Data; using JobTrackerApi.Models; using JobTrackerApi.Services; using JobTrackerApi.Tests.TestSupport; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Moq; using Xunit; namespace JobTrackerApi.Tests; public sealed class SqliteDateTimeOffsetCompatibilityTests { [Fact] public void MariaDb_branch_translates_the_same_date_ordering_and_range_queries() { var currentUser = new Mock(); currentUser.SetupGet(x => x.UserId).Returns("user-1"); var options = new DbContextOptionsBuilder() .UseMySql( "Server=localhost;Database=translation_only;User=test;Password=test", new MariaDbServerVersion(new Version(11, 0, 0))) .Options; using var db = new JobTrackerContext(options, currentUser.Object); var monthStart = new DateTimeOffset(2026, 8, 1, 0, 0, 0, TimeSpan.Zero); var orderedSql = db.CvVariants.Where(x => x.OwnerUserId == "user-1").OrderByDescending(x => x.UpdatedAtUtc).ToQueryString(); var rangeSql = db.AiInteractions.Where(x => x.OwnerUserId == "user-1" && x.CreatedAtUtc >= monthStart).ToQueryString(); Assert.Contains("ORDER BY", orderedSql, StringComparison.OrdinalIgnoreCase); Assert.Contains("CreatedAtUtc", rangeSql, StringComparison.Ordinal); } [Fact] public async Task Affected_workspace_queries_order_dates_and_preserve_owner_scope() { await using var fixture = await SqliteFixture.CreateAsync("user-1"); var db = fixture.Db; var company = new Company { OwnerUserId = "user-1", Name = "Acme" }; db.Companies.Add(company); await db.SaveChangesAsync(); var job = new JobApplication { OwnerUserId = "user-1", CompanyId = company.Id, JobTitle = "Developer", Status = "Applied" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); var older = DateTimeOffset.UtcNow.AddDays(-2); var newer = DateTimeOffset.UtcNow.AddDays(-1); db.CvVariants.AddRange( Variant("user-1", job.Id, "old", older), Variant("user-1", job.Id, "new", newer), Variant("user-2", null, "other", DateTimeOffset.UtcNow)); db.AiInteractions.AddRange( Interaction("user-1", job.Id, older), Interaction("user-1", job.Id, newer), Interaction("user-2", job.Id, DateTimeOffset.UtcNow)); await db.SaveChangesAsync(); var variants = new CvVariantService(db, new CareerProfileService(db), new ThemedCvRenderer()); var history = new AiWorkspaceService(db, Mock.Of()); var listed = await variants.ListAsync("user-1", default); var interactions = await history.HistoryAsync("user-1", job.Id, null, default); var workspace = await new ApplicationWorkspaceService(db, new ApplicationChecklistService(db)) .GetOverviewAsync("user-1", job.Id, default); var variantCatalog = new Mock(); variantCatalog.Setup(x => x.ListAsync("user-1", It.IsAny())).ReturnsAsync(listed); var assets = await new ApplicationAssetsService(db, variantCatalog.Object, new ApplicationIntelligenceService(db, new JobCvMatchService())) .GetCvAsync("user-1", job.Id, default); Assert.Equal(new[] { "new", "old" }, listed.Select(x => x.Name)); Assert.Equal(new[] { newer, older }, interactions.Select(x => x.CreatedAtUtc)); Assert.Equal("new", workspace!.Cv.VariantName); Assert.Equal(newer, workspace.LastAiAtUtc); Assert.Equal(2, workspace.AiInteractionCount); Assert.Equal("new", assets!.AttachedVariantName); } [Fact] public async Task Usage_and_extraction_run_endpoints_work_on_sqlite() { await using var fixture = await SqliteFixture.CreateAsync("user-1"); var db = fixture.Db; var user = new ApplicationUser { Id = "user-1" }; var users = TestHostFactory.CreateUserManager(); users.Setup(x => x.GetUserAsync(It.IsAny())).ReturnsAsync(user); users.Setup(x => x.GetRolesAsync(user)).ReturnsAsync(new[] { "Premium" }); var now = DateTimeOffset.UtcNow; var company = new Company { OwnerUserId = "user-1", Name = "Acme" }; db.Companies.Add(company); await db.SaveChangesAsync(); var job = new JobApplication { OwnerUserId = "user-1", CompanyId = company.Id, JobTitle = "Developer", Status = "Applied" }; db.JobApplications.Add(job); await db.SaveChangesAsync(); db.AiInteractions.AddRange( Interaction("user-1", job.Id, now), Interaction("user-1", job.Id, now.AddMonths(-2)), Interaction("user-2", job.Id, now)); db.CvExtractionRuns.AddRange( Run("user-1", now.AddDays(-1), "old"), Run("user-1", now, "new"), Run("user-2", now.AddDays(1), "other")); await db.SaveChangesAsync(); var usageResult = await new AiUsageController(users.Object, db).Get(default); var usage = Assert.IsType(Assert.IsType(usageResult.Result).Value); Assert.Equal(1, usage.CurrentMonth.Calls); Assert.Equal(2, usage.AllTime.Calls); var workspaceService = new Mock(); workspaceService.SetupGet(x => x.Modules).Returns(Array.Empty()); var generate = await new AiWorkspaceController(users.Object, workspaceService.Object, new ConfigurationBuilder().Build(), db) .Generate(job.Id, new AiWorkspaceController.GenerateRequest("job-analysis", null, null), default); Assert.IsType(generate.Result); var newestArtifactPath = Path.Combine(fixture.Paths.CvArtifactsRoot, "new.txt"); await File.WriteAllTextAsync(newestArtifactPath, "synthetic CV"); db.CvUploadArtifacts.AddRange( new CvUploadArtifact { OwnerUserId = "user-1", OriginalFileName = "old.txt", StoragePath = "missing", Sha256 = "old", ByteSize = 1, UploadedAtUtc = now.AddDays(-1) }, new CvUploadArtifact { OwnerUserId = "user-1", OriginalFileName = "new.txt", StoragePath = newestArtifactPath, Sha256 = "new", ByteSize = 12, UploadedAtUtc = now }); await db.SaveChangesAsync(); var queue = new Mock(); var profile = new ProfileCvController(users.Object, Mock.Of(), db, fixture.Paths, cvProcessingQueue: queue.Object) { ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() } }; var runsResult = await profile.GetRuns(); var runs = Assert.IsAssignableFrom>(Assert.IsType(runsResult.Result).Value).ToList(); Assert.Equal(new[] { "new", "old" }, runs.Select(x => x.Trigger)); Assert.IsType(await profile.Reprocess()); Assert.Equal("new.txt", (await db.CvExtractionRuns.OrderByDescending(x => x.Id).FirstAsync()).Artifact!.OriginalFileName); } private static CvVariant Variant(string owner, int? jobId, string name, DateTimeOffset updated) => new() { OwnerUserId = owner, JobApplicationId = jobId, PublicSlug = Guid.NewGuid().ToString("N"), Name = name, SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings()), CreatedAtUtc = updated, UpdatedAtUtc = updated, }; private static AiInteraction Interaction(string owner, int jobId, DateTimeOffset created) => new() { OwnerUserId = owner, JobApplicationId = jobId, Module = "job-analysis", Title = "Analysis", Provider = "test", ResultJson = "{}", InputCharacterCount = 10, OutputCharacterCount = 5, EstimatedTokenCount = 4, CreatedAtUtc = created, }; private static CvExtractionRun Run(string owner, DateTimeOffset started, string trigger) => new() { OwnerUserId = owner, Trigger = trigger, Status = "applied", ParserVersion = "test", NormalizerVersion = "test", LlmPromptVersion = "test", StartedAtUtc = started, }; private sealed class SqliteFixture : IAsyncDisposable { private readonly SqliteConnection _connection; private readonly string _tempRoot; public JobTrackerContext Db { get; } public AppPaths Paths { get; } private SqliteFixture(SqliteConnection connection, JobTrackerContext db, AppPaths paths, string tempRoot) { _connection = connection; Db = db; Paths = paths; _tempRoot = tempRoot; } public static async Task CreateAsync(string owner) { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var currentUser = new Mock(); currentUser.SetupGet(x => x.UserId).Returns(owner); var db = new JobTrackerContext(new DbContextOptionsBuilder().UseSqlite(connection).Options, currentUser.Object); await db.Database.EnsureCreatedAsync(); var tempRoot = Path.Combine(Path.GetTempPath(), $"jobtracker-sqlite-tests-{Guid.NewGuid():N}"); var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Data:Root"] = tempRoot }).Build(); var environment = new Mock(); environment.SetupGet(x => x.ContentRootPath).Returns(tempRoot); return new SqliteFixture(connection, db, new AppPaths(config, environment.Object), tempRoot); } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await _connection.DisposeAsync(); if (Directory.Exists(_tempRoot)) Directory.Delete(_tempRoot, true); } } }