231 lines
9.2 KiB
C#
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 */ }
|
|
}
|
|
}
|
|
}
|