using System; using System.IO; using System.Linq; using Microsoft.Data.Sqlite; using Xunit; using ytLive.Models; using ytLive.ViewModels; namespace ytLive.Tests; /// /// TASK 25 integration: a pre-consolidation DB (static Background + stray /// "Game Capture" duplicates, wrong flavor on Live, orphaned settings keys) /// heals to the locked model — exactly one Background per screen, index 0, /// right flavor, right name — and a save purges the orphaned keys. /// Real-App + temp-DB pattern (see SourceNamingTests). /// [Collection("RealApp")] public sealed class BackgroundHealIntegrationTests { [Fact] public void Dirty_Layout_Heals_To_One_Locked_Background_Per_Scene() { _app.Run(Run); } private readonly RealAppHost _app; public BackgroundHealIntegrationTests(RealAppHost app) => _app = app; private void Run() { var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-bgheal-{Guid.NewGuid():N}.db"); MainViewModel.LayoutPathOverride = tempDb; var window = default(MainWindow); try { using (var schema = new ytLive.Services.LayoutStore(tempDb)) { } SqliteConnection.ClearAllPools(); SeedDirtyDatabase(tempDb); window = new MainWindow(); var vm = (MainViewModel)window.DataContext; foreach (var name in SceneCatalog.All) { var scene = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, name)); Assert.True(scene.HasBackground, $"{name}: HasBackground must be true"); var background = Assert.Single( scene.Elements.OfType().Where(s => s.IsBackground).ToList(), s => s.IsBackground); Assert.Equal("Background", background.Name); Assert.Equal(0, scene.Elements.IndexOf(background)); var wantedType = SceneCatalog.Is(name, SceneCatalog.Live) ? SourceType.DisplayCapture : SourceType.Background; Assert.True(background.Type == wantedType, $"{name}: expected {wantedType}, got {background.Type}"); } vm.SaveLayoutNow(); using (var connection = new SqliteConnection($"Data Source={tempDb}")) { connection.Open(); using (var dupes = connection.CreateCommand()) { dupes.CommandText = "SELECT COUNT(*) FROM Source WHERE Name = 'Game Capture';"; Assert.Equal(0L, (long)dupes.ExecuteScalar()!); } using (var perScene = connection.CreateCommand()) { perScene.CommandText = @"SELECT s.Name, COUNT(*) FROM Source src JOIN Scene s ON s.Id = src.SceneId WHERE src.IsBackground = 1 GROUP BY s.Name;"; using var reader = perScene.ExecuteReader(); var counts = new System.Collections.Generic.Dictionary(); while (reader.Read()) counts[reader.GetString(0)] = reader.GetInt64(1); var live = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live)); vm.StagedScene = live; // Layering invariant: the Live screen's background is capture-flavored, // so it must never feed ActiveBackgroundImage (that image renders above // the capture and would cover it permanently). Assert.Null(vm.ActiveBackgroundImage); foreach (var name in SceneCatalog.All) Assert.True(counts.TryGetValue(name, out var c) && c == 1, $"{name}: expected exactly 1 background row"); } using (var staleKeys = connection.CreateCommand()) { staleKeys.CommandText = @"SELECT COUNT(*) FROM Settings WHERE (Key LIKE 'BackgroundUseDefault\_%' ESCAPE '\' OR Key LIKE 'BackgroundPath\_%' ESCAPE '\') AND SUBSTR(Key, INSTR(Key, '_') + 1) IN ('bg-stray-1','bg-stray-2');"; Assert.Equal(0L, (long)staleKeys.ExecuteScalar()!); } } } finally { window?.Close(); MainViewModel.LayoutPathOverride = null; SqliteConnection.ClearAllPools(); try { File.Delete(tempDb); } catch { /* best-effort cleanup */ } } } /// Recreates the rot observed in the user's real DB: duplicate /// capture-flavored rows beside static ones, a misnamed Live background, /// and settings keys pointing at rows that shouldn't exist. private static void SeedDirtyDatabase(string path) { using var connection = new SqliteConnection($"Data Source={path}"); connection.Open(); var scenes = new (string Id, string Name)[] { ("scene-starting", "Starting"), ("scene-live", "Live"), ("scene-brb", "BRB"), ("scene-chat", "Chat"), ("scene-ending", "Ending"), }; foreach (var (id, name) in scenes) { using var cmd = connection.CreateCommand(); cmd.CommandText = "INSERT INTO Scene (Id, Name, HasBackground, SortOrder) VALUES ($id, $name, 1, $order);"; cmd.Parameters.AddWithValue("$id", id); cmd.Parameters.AddWithValue("$name", name); cmd.Parameters.AddWithValue("$order", Array.IndexOf(scenes, (id, name))); cmd.ExecuteNonQuery(); } InsertSource(connection, "bg-static-1", "scene-starting", "Background", "Background", 0); InsertSource(connection, "bg-stray-1", "scene-starting", "Game Capture", "DisplayCapture", 1); InsertSource(connection, "bg-live-1", "scene-live", "Game Capture", "DisplayCapture", 0); InsertSource(connection, "bg-static-2", "scene-chat", "Background", "Background", 0); InsertSource(connection, "bg-stray-2", "scene-chat", "Game Capture", "DisplayCapture", 1); using (var key1 = connection.CreateCommand()) { key1.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('BackgroundUseDefault_bg-stray-1', '0');"; key1.ExecuteNonQuery(); } using (var key2 = connection.CreateCommand()) { key2.CommandText = "INSERT INTO Settings (Key, Value) VALUES ('BackgroundPath_bg-stray-2', 'C:\\\\nope.png');"; key2.ExecuteNonQuery(); } } private static void InsertSource( SqliteConnection connection, string id, string sceneId, string name, string type, int sortOrder) { using var cmd = connection.CreateCommand(); cmd.CommandText = @"INSERT INTO Source (Id, SceneId, Type, Name, IsEnabled, X, Y, Width, Height, IsBackground, SortOrder) VALUES ($id, $sceneId, $type, $name, 1, 0, 0, 1920, 1080, 1, $sortOrder);"; cmd.Parameters.AddWithValue("$id", id); cmd.Parameters.AddWithValue("$sceneId", sceneId); cmd.Parameters.AddWithValue("$type", type); cmd.Parameters.AddWithValue("$name", name); cmd.Parameters.AddWithValue("$sortOrder", sortOrder); cmd.ExecuteNonQuery(); } }