Files
LlamaCasty/ytLive.Tests/BroadcastPullOutTests.cs

231 lines
9.2 KiB
C#

using System;
using System.Net;
using System.Net.Http;
using System.Text.Json;
using Xunit;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.Tests;
/// <summary>
/// TASK: broadcast metadata pull-out + launch geometry. One integration slice:
/// (1) pull-out edits persist immediately to the Settings store and re-load,
/// (2) a real MainWindow restores saved size/position, clamping off-screen or
/// below-minimum values into the primary work area, (3) UpdateBroadcast PATCHes
/// liveBroadcasts?part=snippet,status with the full editable field set.
/// </summary>
[Collection("RealApp")]
public sealed class BroadcastPullOutTests
{
public BroadcastPullOutTests(RealAppHost app) => _app = app;
private readonly RealAppHost _app;
[Fact]
public void Metadata_Persists_WindowRestores_Clamped_And_UpdatePatchesRemote()
{
_app.Run(Run);
}
private sealed class StubHandler : HttpMessageHandler
{
public HttpRequestMessage? Request;
public string? Body;
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
Request = request;
Body = request.Content?.ReadAsStringAsync(cancellationToken).GetAwaiter().GetResult();
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("{\"id\":\"bc123\"}", System.Text.Encoding.UTF8, "application/json"),
});
}
}
private void Run()
{
var tempDb = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"ytLlive-pullout-{Guid.NewGuid():N}.db");
ViewModels.MainViewModel.LayoutPathOverride = tempDb;
MainWindow? window = default;
try
{
using (var schema = new LayoutStore(tempDb)) { }
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
var vm = new ViewModels.MainViewModel();
vm.BroadcastForm.Title = "Hello";
vm.BroadcastForm.Description = "World";
vm.BroadcastForm.TagsCsv = "a, b ,c";
vm.BroadcastForm.Visibility = "public";
vm.BroadcastForm.MadeForKids = true;
using (var reader = new LayoutStore(tempDb))
{
var meta = reader.LoadBroadcastMetadata();
Assert.Equal("Hello", meta.Title);
Assert.Equal("World", meta.Description);
Assert.Equal("a, b ,c", meta.TagsCsv);
Assert.Equal("public", meta.Visibility);
Assert.True(meta.MadeForKids);
}
vm.BroadcastForm.Load();
Assert.Equal("Hello", vm.BroadcastForm.Title);
Assert.False(vm.BroadcastForm.IsDrawerOpen);
vm.BroadcastForm.ToggleDrawerCommand.Execute(null);
Assert.True(vm.BroadcastForm.IsDrawerOpen);
vm.BroadcastForm.ToggleDrawerCommand.Execute(null);
Assert.False(vm.BroadcastForm.IsDrawerOpen);
vm.BroadcastForm.ToggleDrawerCommand.Execute(null);
Assert.True(vm.BroadcastForm.IsDrawerOpen);
vm.BroadcastForm.CloseDrawerCommand.Execute(null);
Assert.False(vm.BroadcastForm.IsDrawerOpen);
using (var writer = new LayoutStore(tempDb))
writer.SaveWindowState(100, 50, 1500, 900, "Normal");
window = new MainWindow();
Assert.Equal(1500, window.Width);
Assert.Equal(900, window.Height);
Assert.Equal(100, window.Left);
Assert.Equal(50, window.Top);
window.Close();
window = null;
using (var writer = new LayoutStore(tempDb))
writer.SaveWindowState(-99999, -99999, 800, 500, "Normal");
window = new MainWindow();
var work = System.Windows.SystemParameters.WorkArea;
Assert.Equal(1366, window.Width);
Assert.Equal(768, window.Height);
Assert.InRange(window.Left, work.Left - window.Width + 120, work.Right - 120);
Assert.InRange(window.Top, work.Top, work.Bottom - 80);
var auth = new YouTubeAuthService("test-client", "test-secret");
auth.SetSession(new YouTubeChannel
{
AccessToken = "token",
TokenExpiry = DateTime.UtcNow.AddHours(1),
});
var stub = new StubHandler();
var service = new YouTubeStreamService(auth, new HttpClient(stub));
var error = service.UpdateBroadcast("bc123", new BroadcastMetadata
{
Title = "T",
Description = "D",
TagsCsv = "x,y",
Visibility = "Public",
MadeForKids = true,
ScheduledStartTime = new DateTime(2026, 8, 24, 12, 0, 0, DateTimeKind.Utc),
}).GetAwaiter().GetResult();
Assert.Null(error);
Assert.NotNull(stub.Request);
Assert.Equal(HttpMethod.Put, stub.Request!.Method);
Assert.Equal("https://www.googleapis.com/youtube/v3/liveBroadcasts?part=snippet,status",
stub.Request.RequestUri!.ToString());
using var doc = JsonDocument.Parse(stub.Body!);
var root = doc.RootElement;
Assert.Equal("bc123", root.GetProperty("id").GetString());
Assert.Equal("T", root.GetProperty("snippet").GetProperty("title").GetString());
Assert.Equal(2, root.GetProperty("snippet").GetProperty("tags").GetArrayLength());
Assert.Equal("public", root.GetProperty("status").GetProperty("privacyStatus").GetString());
Assert.True(root.GetProperty("status").GetProperty("selfDeclaredMadeForKids").GetBoolean());
Assert.Equal(new DateTime(2026, 8, 24, 12, 0, 0, DateTimeKind.Utc).ToString("o"),
root.GetProperty("snippet").GetProperty("scheduledStartTime").GetString());
// --- Diff + close drawer tests ---
var auth2 = new YouTubeAuthService("test-client", "test-secret");
auth2.SetSession(new YouTubeChannel
{
AccessToken = "token2",
TokenExpiry = DateTime.UtcNow.AddHours(1),
});
var stub2 = new StubHandler();
var service2 = new YouTubeStreamService(auth2, new HttpClient(stub2));
string? broadcastId = "bc456";
// --- Offline path (isLive = false): saves locally, never pushes API ---
var formOffline = new LiveBroadcastFormViewModel(
() => new LayoutStore(tempDb),
() => service2,
() => broadcastId,
() => false);
formOffline.Load();
Assert.False(formOffline.IsDrawerOpen);
// No edits → stays open, no API call
formOffline.IsDrawerOpen = true;
formOffline.UpdateBroadcastCommand.Execute(null);
Assert.Null(stub2.Request);
Assert.False(formOffline.IsDrawerOpen);
// Edit Title → closes drawer, still no API call (offline = local save only)
stub2.Request = null;
formOffline.Title = "Changed offline";
formOffline.IsDrawerOpen = true;
formOffline.UpdateBroadcastCommand.Execute(null);
Assert.Null(stub2.Request);
Assert.False(formOffline.IsDrawerOpen);
// Re-load → the offline edit persisted to the store
formOffline.Load();
Assert.Equal("Changed offline", formOffline.Title);
// --- Live path (isLive = true): pushes to API ---
var formLive = new LiveBroadcastFormViewModel(
() => new LayoutStore(tempDb),
() => service2,
() => broadcastId,
() => true);
formLive.Load();
formLive.IsDrawerOpen = true;
// No edits → "No changes to update", stays open
stub2.Request = null;
formLive.UpdateBroadcastCommand.Execute(null);
Assert.Null(stub2.Request);
Assert.Equal("No changes to update", formLive.UpdateError);
Assert.True(formLive.IsDrawerOpen);
// Edit Title → HTTP PUT fires, drawer closes
stub2.Request = null;
formLive.Title = "Live update";
formLive.UpdateBroadcastCommand.Execute(null);
Assert.NotNull(stub2.Request);
Assert.Equal(HttpMethod.Put, stub2.Request!.Method);
Assert.Null(formLive.UpdateError);
Assert.False(formLive.IsDrawerOpen);
// Re-seed snapshot via Load, then edit again → second upload fires
stub2.Request = null;
formLive.Load();
formLive.Description = "Live desc";
formLive.UpdateBroadcastCommand.Execute(null);
Assert.NotNull(stub2.Request);
Assert.Null(formLive.UpdateError);
Assert.False(formLive.IsDrawerOpen);
}
finally
{
window?.Close();
ViewModels.MainViewModel.LayoutPathOverride = null;
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
try { System.IO.File.Delete(tempDb); } catch { /* best-effort cleanup */ }
}
}
}