using FluentAssertions; using InboxIntel.Application.Abstractions; using InboxIntel.Domain.Entities; using InboxIntel.Infrastructure.Features; using InboxIntel.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Xunit; namespace InboxIntel.IntegrationTests; /// /// Feature-flag + AI-gate contracts (docs/discovery/multi-provider/04): /// flags are FAIL-CLOSED, and the admin master switch (ai.enabled) beats any user opt-in. /// public class FeatureFlagTests { private sealed class FakeCurrentUser : ICurrentUser { public Guid UserId { get; set; } public bool IsAuthenticated => UserId != Guid.Empty; } private static AppDbContext NewDb(string name) => new(new DbContextOptionsBuilder().UseInMemoryDatabase(name).Options, new FakeCurrentUser()); private static FeatureFlagService Flags(AppDbContext db) => new(db, new MemoryCache(new MemoryCacheOptions())); [Fact] public async Task Unknown_flag_is_disabled_fail_closed() { using var db = NewDb(nameof(Unknown_flag_is_disabled_fail_closed)); (await Flags(db).IsEnabledAsync("does.not.exist")).Should().BeFalse(); } [Fact] public async Task Enabled_flag_reads_true_disabled_reads_false() { using var db = NewDb(nameof(Enabled_flag_reads_true_disabled_reads_false)); db.FeatureFlags.AddRange( new FeatureFlag { Key = "on.flag", Enabled = true }, new FeatureFlag { Key = "off.flag", Enabled = false }); await db.SaveChangesAsync(); var flags = Flags(db); (await flags.IsEnabledAsync("on.flag")).Should().BeTrue(); (await flags.IsEnabledAsync("off.flag")).Should().BeFalse(); } [Fact] public async Task Ai_gate_master_flag_off_beats_user_opt_in() { using var db = NewDb(nameof(Ai_gate_master_flag_off_beats_user_opt_in)); var user = Guid.NewGuid(); db.FeatureFlags.Add(new FeatureFlag { Key = AiGate.MasterFlag, Enabled = false }); db.UserSettings.Add(new UserSetting { UserId = user, AiOptIn = true }); await db.SaveChangesAsync(); (await new AiGate(Flags(db), db).IsAiEnabledForUserAsync(user)).Should().BeFalse(); } [Fact] public async Task Ai_gate_flag_on_defaults_to_opted_in_but_respects_opt_out() { using var db = NewDb(nameof(Ai_gate_flag_on_defaults_to_opted_in_but_respects_opt_out)); var optedOut = Guid.NewGuid(); var noRow = Guid.NewGuid(); db.FeatureFlags.Add(new FeatureFlag { Key = AiGate.MasterFlag, Enabled = true }); db.UserSettings.Add(new UserSetting { UserId = optedOut, AiOptIn = false }); await db.SaveChangesAsync(); var gate = new AiGate(Flags(db), db); (await gate.IsAiEnabledForUserAsync(noRow)).Should().BeTrue("no settings row = default opt-in"); (await gate.IsAiEnabledForUserAsync(optedOut)).Should().BeFalse("explicit opt-out wins while the flag is on"); } }