OAuth session persists via DPAPI; sign-in lives in the Start Stream dialog; add xUnit test project (6 passing)

This commit is contained in:
2026-08-06 09:38:35 -07:00
parent 8f0ecd3796
commit 4afe11e718
15 changed files with 432 additions and 67 deletions
+62
View File
@@ -0,0 +1,62 @@
using System.IO;
using Xunit;
using ytLive.Helpers;
using ytLive.Models;
namespace ytLive.Tests;
public class TokenStoreTests
{
private static string TempPath() =>
Path.Combine(Path.GetTempPath(), "ytLive_tests", $"{Guid.NewGuid():N}.auth");
[Fact]
public void Save_Load_RoundtripsChannelAndTokens()
{
var path = TempPath();
var channel = new YouTubeChannel
{
ChannelId = "UC123",
DisplayName = "Test Channel",
ProfileImageUrl = "http://example/thumb.jpg",
AccessToken = "acc-token",
RefreshToken = "refresh-token",
TokenExpiry = DateTime.UtcNow.AddHours(1),
};
TokenStore.Save(channel, path);
var loaded = TokenStore.Load(path);
Assert.NotNull(loaded);
Assert.Equal("UC123", loaded!.ChannelId);
Assert.Equal("Test Channel", loaded.DisplayName);
Assert.Equal("http://example/thumb.jpg", loaded.ProfileImageUrl);
Assert.Equal("acc-token", loaded.AccessToken);
Assert.Equal("refresh-token", loaded.RefreshToken);
Assert.Equal(channel.TokenExpiry, loaded.TokenExpiry);
}
[Fact]
public void Load_MissingFile_ReturnsNull()
{
Assert.Null(TokenStore.Load(TempPath()));
}
[Fact]
public void Load_CorruptFile_ReturnsNull()
{
var path = TempPath();
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllBytes(path, [1, 2, 3, 4, 5]);
Assert.Null(TokenStore.Load(path));
}
[Fact]
public void Clear_RemovesSavedToken()
{
var path = TempPath();
TokenStore.Save(new YouTubeChannel { DisplayName = "X" }, path);
TokenStore.Clear(path);
Assert.Null(TokenStore.Load(path));
}
}
+74
View File
@@ -0,0 +1,74 @@
using System.Net;
using System.Text;
using Xunit;
using ytLive.Models;
using ytLive.Services;
namespace ytLive.Tests;
public class YouTubeAuthServiceTests
{
private sealed class FakeHandler : HttpMessageHandler
{
private readonly string _tokenResponse;
private readonly string _channelResponse;
public FakeHandler(string tokenResponse, string channelResponse)
{
_tokenResponse = tokenResponse;
_channelResponse = channelResponse;
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
var isToken = request.RequestUri!.PathAndQuery.Contains("/token");
var body = isToken ? _tokenResponse : _channelResponse;
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
}
}
private static YouTubeAuthService CreateService(string tokenResponse, string channelResponse) =>
new("test-id", "test-secret", new HttpClient(new FakeHandler(tokenResponse, channelResponse)));
[Fact]
public async Task ExchangeCodeForToken_MockServer_ParsesChannel()
{
var service = CreateService(
"""{"access_token":"acc-123","refresh_token":"ref-123","expires_in":3600}""",
"""{"items":[{"id":"UC123","snippet":{"title":"Test Channel","thumbnails":{"default":{"url":"http://example/thumb.jpg"}}}}]}""");
var channel = await service.ExchangeCodeForToken("auth-code", "http://localhost:8765/oauth2/callback");
Assert.NotNull(channel);
Assert.Equal("UC123", channel!.ChannelId);
Assert.Equal("Test Channel", channel.DisplayName);
Assert.Equal("http://example/thumb.jpg", channel.ProfileImageUrl);
Assert.Equal("acc-123", channel.AccessToken);
Assert.Equal("ref-123", channel.RefreshToken);
Assert.InRange(channel.TokenExpiry, DateTime.UtcNow.AddSeconds(3590), DateTime.UtcNow.AddSeconds(3610));
}
[Fact]
public async Task RefreshToken_MockServer_UpdatesAccessTokenAndExpiry()
{
var service = CreateService(
"""{"access_token":"new-acc","expires_in":3600}""",
"""{"items":[]}""");
service.SetSession(new YouTubeChannel
{
RefreshToken = "ref-123",
TokenExpiry = DateTime.UtcNow.AddMinutes(-5),
});
var ok = await service.RefreshToken();
Assert.True(ok);
Assert.Equal("new-acc", service.CurrentChannel!.AccessToken);
Assert.InRange(service.CurrentChannel.TokenExpiry,
DateTime.UtcNow.AddSeconds(3590), DateTime.UtcNow.AddSeconds(3610));
}
}
+21
View File
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1"/>
<PackageReference Include="xunit" Version="2.9.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ytLive.csproj"/>
</ItemGroup>
</Project>