using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using Tau.Acuvim.Portal.Configuration; using Tau.Acuvim.Portal.Data; using Tau.Acuvim.Portal.DTOs; using Tau.Acuvim.Portal.Services; namespace Tau.Acuvim.Portal.Tests; // Proves the "pre-brand at spin-up" flow end-to-end at the C# level: // docker-compose WhiteLabel__* env var → IOptions // → BrandingService.SeedAsync → WhiteLabelSettings row on first read. // // The Docker passthrough that bridges env-var → container is pure YAML and // covered by the integration spin-up; this test pins the C# half so we'd // catch a regression in BrandingService that broke the contract (e.g. someone // changing SeedAsync to use a constructor literal instead of IOptions). public class BrandingSeedFromOptionsTests { private static AppDbContext NewDb() => new(new DbContextOptionsBuilder() .UseInMemoryDatabase("brand-" + Guid.NewGuid()) .Options); private static BrandingService NewService(AppDbContext db, WhiteLabelOptions opts) => new(db, Options.Create(opts)); private static WhiteLabelOptions AcmeBrand() => new() { ApplicationName = "Acme Power Monitoring", LogoUrl = "https://cdn.acme.example/logo.png", PrimaryColor = "#0c4a6e", SecondaryColor = "#0e7490", AccentColor = "#06b6d4", FooterText = "© Acme Corp", }; [Fact] public async Task FirstGet_SeedsRowFromOptions() { using var db = NewDb(); var svc = NewService(db, AcmeBrand()); var result = await svc.GetAsync(); Assert.Equal("Acme Power Monitoring", result.ApplicationName); Assert.Equal("https://cdn.acme.example/logo.png", result.LogoUrl); Assert.Equal("#0c4a6e", result.PrimaryColor); Assert.Equal("#0e7490", result.SecondaryColor); Assert.Equal("#06b6d4", result.AccentColor); Assert.Equal("© Acme Corp", result.FooterText); } [Fact] public async Task SecondGet_DoesNotReseed_EvenIfOptionsChange() { using var db = NewDb(); var first = NewService(db, AcmeBrand()); await first.GetAsync(); // Operator "changes the template defaults" — but the row already exists // in the DB, so the UI-authoritative value must win on subsequent gets. // This is the contract that makes Settings → Branding actually durable. var changedDefaults = new WhiteLabelOptions { ApplicationName = "Different Defaults", PrimaryColor = "#ff0000", SecondaryColor = "#00ff00", AccentColor = "#0000ff", FooterText = "different", }; var second = NewService(db, changedDefaults); var result = await second.GetAsync(); Assert.Equal("Acme Power Monitoring", result.ApplicationName); Assert.Equal("#0c4a6e", result.PrimaryColor); } [Fact] public async Task EnsureSeededAsync_WritesRowImmediately_NoGetNeeded() { using var db = NewDb(); var svc = NewService(db, AcmeBrand()); await svc.EnsureSeededAsync(); var row = await db.WhiteLabelSettings.AsNoTracking().SingleAsync(); Assert.Equal("Acme Power Monitoring", row.ApplicationName); Assert.Equal("#06b6d4", row.AccentColor); } [Fact] public async Task EnsureSeededAsync_IsIdempotent() { using var db = NewDb(); var svc = NewService(db, AcmeBrand()); await svc.EnsureSeededAsync(); await svc.EnsureSeededAsync(); await svc.EnsureSeededAsync(); var rowCount = await db.WhiteLabelSettings.CountAsync(); Assert.Equal(1, rowCount); } [Fact] public async Task UpdateAsync_BlankFields_FallBackToOptionsDefaults() { using var db = NewDb(); var svc = NewService(db, AcmeBrand()); await svc.GetAsync(); // seed // Operator clears the application name in the UI — service should // fall back to the IOptions default rather than persist empty string. var updated = await svc.UpdateAsync(new UpdateBrandingRequest( ApplicationName: "", PrimaryColor: "#abc123", SecondaryColor: "", AccentColor: "", FooterText: "kept", LogoUrl: null)); Assert.Equal("Acme Power Monitoring", updated.ApplicationName); Assert.Equal("#abc123", updated.PrimaryColor); Assert.Equal("#0e7490", updated.SecondaryColor); Assert.Equal("#06b6d4", updated.AccentColor); Assert.Equal("kept", updated.FooterText); } }