feat(worker,ui): Online Inbox config + auth hub plumbing (Phase 2)
Hub: GetOnlineInboxState / SetOnlineInboxConfig / SetOnlineInboxAuth / ClearOnlineInboxAuth. WorkerConfig.SaveOnlineInbox persists only the online_inbox section. OnlineTokenStore + config registered always so hub methods work when sync is disabled. IWorkerClient surface + all test fakes synced. RedirectUri config (default http://localhost:8765/callback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,5 +108,10 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
||||
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||
|
||||
public virtual Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
||||
public virtual Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
||||
public virtual Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
||||
public virtual Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
||||
|
||||
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ public sealed class ClearMyDayHubTests : IDisposable
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore());
|
||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return hub;
|
||||
|
||||
174
tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
Normal file
174
tests/ClaudeDo.Worker.Tests/Hub/OnlineInboxHubTests.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Online;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Xunit;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Hub;
|
||||
|
||||
public sealed class OnlineInboxHubTests : IDisposable
|
||||
{
|
||||
private readonly string _configPath = Path.Combine(
|
||||
Path.GetTempPath(), $"online_inbox_hub_{Guid.NewGuid():N}.json");
|
||||
private readonly string _tokenPath = Path.Combine(
|
||||
Path.GetTempPath(), $"online_inbox_hub_token_{Guid.NewGuid():N}.bin");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_configPath); } catch { }
|
||||
try { File.Delete(_tokenPath); } catch { }
|
||||
}
|
||||
|
||||
private (WorkerHub hub, OnlineInboxConfig inboxCfg, OnlineTokenStore store) CreateHub(
|
||||
OnlineInboxConfig? inboxCfg = null, OnlineTokenStore? store = null)
|
||||
{
|
||||
var cfg = new WorkerConfig();
|
||||
inboxCfg ??= cfg.OnlineInbox;
|
||||
store ??= new OnlineTokenStore(_tokenPath);
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, broadcaster, null!,
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
cfg, inboxCfg, store);
|
||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return (hub, inboxCfg, store);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOnlineInboxState_ReflectsConfigAndNoToken()
|
||||
{
|
||||
var (hub, _, _) = CreateHub();
|
||||
|
||||
var state = hub.GetOnlineInboxState();
|
||||
|
||||
Assert.False(state.Enabled);
|
||||
Assert.False(state.SignedIn);
|
||||
Assert.Equal("", state.ApiBaseUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOnlineInboxState_SignedIn_WhenTokenPresent()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return; // DPAPI is Windows-only
|
||||
|
||||
var (hub, _, store) = CreateHub();
|
||||
store.Save("my-refresh-token");
|
||||
|
||||
var state = hub.GetOnlineInboxState();
|
||||
|
||||
Assert.True(state.SignedIn);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOnlineInboxState_ReflectsConfigFields()
|
||||
{
|
||||
var inboxCfg = new OnlineInboxConfig
|
||||
{
|
||||
Enabled = true,
|
||||
ApiBaseUrl = "https://inbox.example.com",
|
||||
RedirectUri = "http://localhost:9999/callback",
|
||||
Zitadel = new ZitadelClientConfig
|
||||
{
|
||||
Authority = "https://auth.example.com",
|
||||
ClientId = "client-abc",
|
||||
Scopes = "openid offline_access",
|
||||
},
|
||||
};
|
||||
var (hub, _, _) = CreateHub(inboxCfg);
|
||||
|
||||
var state = hub.GetOnlineInboxState();
|
||||
|
||||
Assert.True(state.Enabled);
|
||||
Assert.Equal("https://inbox.example.com", state.ApiBaseUrl);
|
||||
Assert.Equal("https://auth.example.com", state.Authority);
|
||||
Assert.Equal("client-abc", state.ClientId);
|
||||
Assert.Equal("openid offline_access", state.Scopes);
|
||||
Assert.Equal("http://localhost:9999/callback", state.RedirectUri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOnlineInboxConfig_UpdatesConfigAndPersistsToFile()
|
||||
{
|
||||
var cfg = new WorkerConfig();
|
||||
// point SaveOnlineInbox to our temp file
|
||||
var inboxCfg = cfg.OnlineInbox;
|
||||
var store = new OnlineTokenStore(_tokenPath);
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
|
||||
// Patch cfg to save to our temp path by writing an initial file there
|
||||
File.WriteAllText(_configPath, "{}");
|
||||
|
||||
// We need to use the internal SaveOnlineInbox with our path.
|
||||
// WorkerHub calls _cfg.SaveOnlineInbox() which uses DefaultConfigPath.
|
||||
// For this test, directly exercise the round-trip via WorkerConfig.SaveOnlineInbox.
|
||||
cfg.OnlineInbox.Enabled = true;
|
||||
cfg.OnlineInbox.ApiBaseUrl = "https://inbox.test.com";
|
||||
cfg.OnlineInbox.PollIntervalSeconds = 120;
|
||||
cfg.OnlineInbox.RedirectUri = "http://localhost:7777/cb";
|
||||
cfg.OnlineInbox.Zitadel.Authority = "https://auth.test.com";
|
||||
cfg.OnlineInbox.Zitadel.ClientId = "test-client";
|
||||
cfg.OnlineInbox.Zitadel.Scopes = "openid offline_access";
|
||||
|
||||
cfg.SaveOnlineInbox(_configPath);
|
||||
|
||||
// Verify file contains only online_inbox node and preserves others
|
||||
var json = File.ReadAllText(_configPath);
|
||||
var root = JsonNode.Parse(json)!.AsObject();
|
||||
|
||||
Assert.NotNull(root["online_inbox"]);
|
||||
var inbox = root["online_inbox"]!.AsObject();
|
||||
Assert.True(inbox["enabled"]!.GetValue<bool>());
|
||||
Assert.Equal("https://inbox.test.com", inbox["api_base_url"]!.GetValue<string>());
|
||||
Assert.Equal(120, inbox["poll_interval_seconds"]!.GetValue<int>());
|
||||
Assert.Equal("http://localhost:7777/cb", inbox["redirect_uri"]!.GetValue<string>());
|
||||
|
||||
// Verify existing file keys are preserved (not overwritten)
|
||||
var roundTripped = WorkerConfig.Load(_configPath);
|
||||
Assert.Equal("https://inbox.test.com", roundTripped.OnlineInbox.ApiBaseUrl);
|
||||
Assert.Equal(true, roundTripped.OnlineInbox.Enabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOnlineInboxConfig_PreservesOtherConfigKeys()
|
||||
{
|
||||
// Write a config with a non-inbox key
|
||||
var initial = """{"signalr_port":12345}""";
|
||||
File.WriteAllText(_configPath, initial);
|
||||
|
||||
var cfg = new WorkerConfig();
|
||||
cfg.OnlineInbox.ApiBaseUrl = "https://new.example.com";
|
||||
cfg.SaveOnlineInbox(_configPath);
|
||||
|
||||
var root = JsonNode.Parse(File.ReadAllText(_configPath))!.AsObject();
|
||||
Assert.Equal(12345, root["signalr_port"]!.GetValue<int>());
|
||||
Assert.Equal("https://new.example.com",
|
||||
root["online_inbox"]!["api_base_url"]!.GetValue<string>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetOnlineInboxAuth_WritesToken()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
|
||||
var (hub, _, store) = CreateHub();
|
||||
hub.SetOnlineInboxAuth("refresh-abc");
|
||||
|
||||
Assert.Equal("refresh-abc", store.Read());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearOnlineInboxAuth_RemovesToken()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
|
||||
var (hub, _, store) = CreateHub();
|
||||
store.Save("some-token");
|
||||
|
||||
hub.ClearOnlineInboxAuth();
|
||||
|
||||
Assert.Null(store.Read());
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,8 @@ public sealed class PlanningHubTests : IDisposable
|
||||
{
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
_planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
_planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore());
|
||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return hub;
|
||||
|
||||
@@ -19,7 +19,8 @@ public sealed class WorktreeStateHubTests : IDisposable
|
||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||
var hub = new WorkerHub(
|
||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!);
|
||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore());
|
||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||
hub.Context = new FakeHubCallerContext();
|
||||
return hub;
|
||||
|
||||
@@ -112,6 +112,10 @@ sealed class FakeWorkerClient : IWorkerClient
|
||||
public Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
||||
public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);
|
||||
public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
||||
public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
||||
public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
||||
public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user