refactor(worker): OnlineTokenStore zu DpapiTokenStore verallgemeinert
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Runtime.Versioning;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persists a single secret encrypted with DPAPI (CurrentUser scope), one instance per file.
|
||||||
|
/// Windows-only. Used for the Online-Inbox refresh token (~/.claudeDo/online-inbox.token) and
|
||||||
|
/// the ticket-system PAT (~/.claudeDo/ticket.pat).
|
||||||
|
/// </summary>
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
public sealed class DpapiTokenStore
|
||||||
|
{
|
||||||
|
private readonly string _tokenPath;
|
||||||
|
|
||||||
|
public DpapiTokenStore(string tokenPath) => _tokenPath = tokenPath;
|
||||||
|
|
||||||
|
public static DpapiTokenStore InAppData(string fileName)
|
||||||
|
=> new(Path.Combine(Paths.AppDataRoot(), fileName));
|
||||||
|
|
||||||
|
public void Save(string secret)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(secret);
|
||||||
|
var cipher = ProtectedData.Protect(Encoding.UTF8.GetBytes(secret), null, DataProtectionScope.CurrentUser);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(_tokenPath)!);
|
||||||
|
File.WriteAllBytes(_tokenPath, cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Read()
|
||||||
|
{
|
||||||
|
if (!File.Exists(_tokenPath)) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var plain = ProtectedData.Unprotect(File.ReadAllBytes(_tokenPath), null, DataProtectionScope.CurrentUser);
|
||||||
|
return Encoding.UTF8.GetString(plain);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists() => File.Exists(_tokenPath);
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
if (File.Exists(_tokenPath)) File.Delete(_tokenPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DI-Marker: der Online-Inbox-Refresh-Token.
|
||||||
|
public sealed record OnlineRefreshTokenStore(DpapiTokenStore Store);
|
||||||
|
|
||||||
|
/// DI-Marker: der Personal Access Token des Ticketsystems.
|
||||||
|
public sealed record TicketPatStore(DpapiTokenStore Store);
|
||||||
@@ -69,7 +69,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly RefineRunner _refineRunner;
|
private readonly RefineRunner _refineRunner;
|
||||||
private readonly WorkerConfig _cfg;
|
private readonly WorkerConfig _cfg;
|
||||||
private readonly OnlineInboxConfig _onlineInboxConfig;
|
private readonly OnlineInboxConfig _onlineInboxConfig;
|
||||||
private readonly OnlineTokenStore _onlineTokenStore;
|
private readonly DpapiTokenStore _onlineTokenStore;
|
||||||
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
|
private readonly Runner.PendingQuestionRegistry _pendingQuestions;
|
||||||
private readonly LogRingBuffer? _logBuffer;
|
private readonly LogRingBuffer? _logBuffer;
|
||||||
private readonly ISessionSkillRegistry _skillRegistry;
|
private readonly ISessionSkillRegistry _skillRegistry;
|
||||||
@@ -104,7 +104,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
RefineRunner refineRunner,
|
RefineRunner refineRunner,
|
||||||
WorkerConfig cfg,
|
WorkerConfig cfg,
|
||||||
OnlineInboxConfig onlineInboxConfig,
|
OnlineInboxConfig onlineInboxConfig,
|
||||||
OnlineTokenStore onlineTokenStore,
|
OnlineRefreshTokenStore onlineTokenStore,
|
||||||
Runner.PendingQuestionRegistry pendingQuestions,
|
Runner.PendingQuestionRegistry pendingQuestions,
|
||||||
ISessionSkillRegistry skillRegistry,
|
ISessionSkillRegistry skillRegistry,
|
||||||
LogRingBuffer? logBuffer = null,
|
LogRingBuffer? logBuffer = null,
|
||||||
@@ -138,7 +138,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
_refineRunner = refineRunner;
|
_refineRunner = refineRunner;
|
||||||
_cfg = cfg;
|
_cfg = cfg;
|
||||||
_onlineInboxConfig = onlineInboxConfig;
|
_onlineInboxConfig = onlineInboxConfig;
|
||||||
_onlineTokenStore = onlineTokenStore;
|
_onlineTokenStore = onlineTokenStore.Store;
|
||||||
_pendingQuestions = pendingQuestions;
|
_pendingQuestions = pendingQuestions;
|
||||||
_skillRegistry = skillRegistry;
|
_skillRegistry = skillRegistry;
|
||||||
_logBuffer = logBuffer;
|
_logBuffer = logBuffer;
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
using System.Runtime.Versioning;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using ClaudeDo.Data;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Online;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Persists the Zitadel refresh token encrypted with DPAPI (CurrentUser scope).
|
|
||||||
/// Windows-only; the file lives at ~/.claudeDo/online-inbox.token.
|
|
||||||
/// </summary>
|
|
||||||
[SupportedOSPlatform("windows")]
|
|
||||||
public sealed class OnlineTokenStore
|
|
||||||
{
|
|
||||||
private readonly string _tokenPath;
|
|
||||||
|
|
||||||
public OnlineTokenStore()
|
|
||||||
: this(Path.Combine(Paths.AppDataRoot(), "online-inbox.token")) { }
|
|
||||||
|
|
||||||
internal OnlineTokenStore(string tokenPath)
|
|
||||||
{
|
|
||||||
_tokenPath = tokenPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Save(string refreshToken)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(refreshToken);
|
|
||||||
var plain = Encoding.UTF8.GetBytes(refreshToken);
|
|
||||||
var cipher = ProtectedData.Protect(plain, null, DataProtectionScope.CurrentUser);
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_tokenPath)!);
|
|
||||||
File.WriteAllBytes(_tokenPath, cipher);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? Read()
|
|
||||||
{
|
|
||||||
if (!File.Exists(_tokenPath)) return null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var cipher = File.ReadAllBytes(_tokenPath);
|
|
||||||
var plain = ProtectedData.Unprotect(cipher, null, DataProtectionScope.CurrentUser);
|
|
||||||
return Encoding.UTF8.GetString(plain);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
if (File.Exists(_tokenPath))
|
|
||||||
File.Delete(_tokenPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,7 @@ namespace ClaudeDo.Worker.Online;
|
|||||||
public sealed class ZitadelAuthProvider : IOnlineAuthProvider
|
public sealed class ZitadelAuthProvider : IOnlineAuthProvider
|
||||||
{
|
{
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly OnlineTokenStore _tokenStore;
|
private readonly DpapiTokenStore _tokenStore;
|
||||||
private readonly OnlineInboxConfig _config;
|
private readonly OnlineInboxConfig _config;
|
||||||
private readonly ILogger<ZitadelAuthProvider> _logger;
|
private readonly ILogger<ZitadelAuthProvider> _logger;
|
||||||
|
|
||||||
@@ -29,12 +29,12 @@ public sealed class ZitadelAuthProvider : IOnlineAuthProvider
|
|||||||
|
|
||||||
public ZitadelAuthProvider(
|
public ZitadelAuthProvider(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
OnlineTokenStore tokenStore,
|
OnlineRefreshTokenStore tokenStore,
|
||||||
OnlineInboxConfig config,
|
OnlineInboxConfig config,
|
||||||
ILogger<ZitadelAuthProvider> logger)
|
ILogger<ZitadelAuthProvider> logger)
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_tokenStore = tokenStore;
|
_tokenStore = tokenStore.Store;
|
||||||
_config = config;
|
_config = config;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Threading;
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Git;
|
using ClaudeDo.Data.Git;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
|
using ClaudeDo.Worker;
|
||||||
using ClaudeDo.Worker.Agents;
|
using ClaudeDo.Worker.Agents;
|
||||||
using ClaudeDo.Worker.Config;
|
using ClaudeDo.Worker.Config;
|
||||||
using ClaudeDo.Worker.External;
|
using ClaudeDo.Worker.External;
|
||||||
@@ -206,12 +207,13 @@ builder.Services.AddMcpServer()
|
|||||||
.WithTools<TaskRunMcpService>()
|
.WithTools<TaskRunMcpService>()
|
||||||
.WithTools<TaskRunFindingsMcpTools>();
|
.WithTools<TaskRunFindingsMcpTools>();
|
||||||
|
|
||||||
// OnlineInboxConfig and OnlineTokenStore are always registered so hub methods work
|
// OnlineInboxConfig and the refresh-token store are always registered so hub methods work
|
||||||
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
|
// even when sync is disabled. The sync stack (api client, auth, hosted service) is
|
||||||
// only registered when enabled.
|
// only registered when enabled.
|
||||||
builder.Services.AddSingleton(cfg.OnlineInbox);
|
builder.Services.AddSingleton(cfg.OnlineInbox);
|
||||||
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI is fine here.
|
||||||
builder.Services.AddSingleton<OnlineTokenStore>();
|
builder.Services.AddSingleton(new OnlineRefreshTokenStore(DpapiTokenStore.InAppData("online-inbox.token")));
|
||||||
|
builder.Services.AddSingleton(new TicketPatStore(DpapiTokenStore.InAppData("ticket.pat")));
|
||||||
#pragma warning restore CA1416
|
#pragma warning restore CA1416
|
||||||
|
|
||||||
if (cfg.OnlineInbox.Enabled)
|
if (cfg.OnlineInbox.Enabled)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class ClearMyDayHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class DeleteTaskHubTests : IDisposable
|
|||||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
built.State,
|
built.State,
|
||||||
null!, null!,
|
null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public sealed class ListConfigHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, factory,
|
null!, null!, null!, null!, broadcaster, factory,
|
||||||
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public sealed class MergeHelperTaskHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
||||||
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
|
||||||
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git,
|
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git,
|
||||||
interactiveReviewSubmission: reviewSubmission);
|
interactiveReviewSubmission: reviewSubmission);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using ClaudeDo.Worker;
|
||||||
using ClaudeDo.Worker.Config;
|
using ClaudeDo.Worker.Config;
|
||||||
using ClaudeDo.Worker.Hub;
|
using ClaudeDo.Worker.Hub;
|
||||||
using ClaudeDo.Worker.Online;
|
using ClaudeDo.Worker.Online;
|
||||||
@@ -21,17 +22,17 @@ public sealed class OnlineInboxHubTests : IDisposable
|
|||||||
try { File.Delete(_tokenPath); } catch { }
|
try { File.Delete(_tokenPath); } catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
private (WorkerHub hub, OnlineInboxConfig inboxCfg, OnlineTokenStore store) CreateHub(
|
private (WorkerHub hub, OnlineInboxConfig inboxCfg, DpapiTokenStore store) CreateHub(
|
||||||
OnlineInboxConfig? inboxCfg = null, OnlineTokenStore? store = null)
|
OnlineInboxConfig? inboxCfg = null, DpapiTokenStore? store = null)
|
||||||
{
|
{
|
||||||
var cfg = new WorkerConfig();
|
var cfg = new WorkerConfig();
|
||||||
inboxCfg ??= cfg.OnlineInbox;
|
inboxCfg ??= cfg.OnlineInbox;
|
||||||
store ??= new OnlineTokenStore(_tokenPath);
|
store ??= new DpapiTokenStore(_tokenPath);
|
||||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, null!,
|
null!, null!, null!, null!, broadcaster, 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!,
|
||||||
cfg, inboxCfg, store, new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
cfg, inboxCfg, new OnlineRefreshTokenStore(store), new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
return (hub, inboxCfg, store);
|
return (hub, inboxCfg, store);
|
||||||
@@ -95,7 +96,7 @@ public sealed class OnlineInboxHubTests : IDisposable
|
|||||||
var cfg = new WorkerConfig();
|
var cfg = new WorkerConfig();
|
||||||
// point SaveOnlineInbox to our temp file
|
// point SaveOnlineInbox to our temp file
|
||||||
var inboxCfg = cfg.OnlineInbox;
|
var inboxCfg = cfg.OnlineInbox;
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
var store = new DpapiTokenStore(_tokenPath);
|
||||||
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
var broadcaster = new HubBroadcaster(new CapturingHubContext());
|
||||||
|
|
||||||
// Patch cfg to save to our temp path by writing an initial file there
|
// Patch cfg to save to our temp path by writing an initial file there
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public sealed class PlanningHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, _db.CreateFactory(), null!, null!, null!,
|
null!, null!, null!, null!, null!, _db.CreateFactory(), 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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed class RepoImportFoldersHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class SessionSkillsHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), registry);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), registry);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class SetTaskStatusHubTests : IDisposable
|
|||||||
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
built.State,
|
built.State,
|
||||||
null!, null!,
|
null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public sealed class TaskDoneDequeueHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
||||||
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(_proxy);
|
hub.Clients = new FakeHubCallerClients(_proxy);
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public sealed class TaskUsageHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public sealed class WorkerBuildInfoHubTests
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
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!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!, null!,
|
||||||
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.Online.OnlineTokenStore(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
|
|
||||||
// No assertion on the actual SHA value (depends on the build environment) — just that
|
// No assertion on the actual SHA value (depends on the build environment) — just that
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class WorktreeStateHubTests : IDisposable
|
|||||||
var hub = new WorkerHub(
|
var hub = new WorkerHub(
|
||||||
null!, null!, null!, null!, broadcaster, _db.CreateFactory(),
|
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(),
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
||||||
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
|
||||||
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
hub.Clients = new FakeHubCallerClients(new RecordingClientProxy());
|
||||||
hub.Context = new FakeHubCallerContext();
|
hub.Context = new FakeHubCallerContext();
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
using ClaudeDo.Worker.Online;
|
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Tests.Online;
|
|
||||||
|
|
||||||
public sealed class OnlineTokenStoreTests : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _tokenPath = Path.Combine(Path.GetTempPath(), $"online_token_{Guid.NewGuid():N}.bin");
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
try { File.Delete(_tokenPath); } catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Save_Read_RoundTrips()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows()) return; // DPAPI is Windows-only
|
|
||||||
|
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
|
||||||
store.Save("my-refresh-token");
|
|
||||||
var result = store.Read();
|
|
||||||
Assert.Equal("my-refresh-token", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Clear_Removes_Token()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows()) return;
|
|
||||||
|
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
|
||||||
store.Save("token");
|
|
||||||
store.Clear();
|
|
||||||
Assert.Null(store.Read());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Read_WhenFileAbsent_Returns_Null()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows()) return;
|
|
||||||
|
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
|
||||||
Assert.Null(store.Read());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Clear_WhenFileAbsent_DoesNotThrow()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows()) return;
|
|
||||||
|
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
|
||||||
store.Clear(); // no exception expected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using ClaudeDo.Worker;
|
||||||
using ClaudeDo.Worker.Online;
|
using ClaudeDo.Worker.Online;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
@@ -8,8 +9,8 @@ namespace ClaudeDo.Worker.Tests.Online;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tests for <see cref="ZitadelAuthProvider"/> using a stub <see cref="HttpMessageHandler"/>.
|
/// Tests for <see cref="ZitadelAuthProvider"/> using a stub <see cref="HttpMessageHandler"/>.
|
||||||
/// Token-store tests use a real temp-dir <see cref="OnlineTokenStore"/> (DPAPI) and are
|
/// Token-store tests use a real temp-dir <see cref="DpapiTokenStore"/> (DPAPI) and are
|
||||||
/// Windows-only, consistent with <see cref="OnlineTokenStoreTests"/>.
|
/// Windows-only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ZitadelAuthProviderTests : IDisposable
|
public sealed class ZitadelAuthProviderTests : IDisposable
|
||||||
{
|
{
|
||||||
@@ -86,14 +87,14 @@ public sealed class ZitadelAuthProviderTests : IDisposable
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
private (ZitadelAuthProvider Provider, StubHandler Handler, OnlineTokenStore Store) Build()
|
private (ZitadelAuthProvider Provider, StubHandler Handler, DpapiTokenStore Store) Build()
|
||||||
{
|
{
|
||||||
var handler = new StubHandler();
|
var handler = new StubHandler();
|
||||||
var factory = new StubHttpClientFactory(handler);
|
var factory = new StubHttpClientFactory(handler);
|
||||||
var store = new OnlineTokenStore(_tokenPath);
|
var store = new DpapiTokenStore(_tokenPath);
|
||||||
var config = MakeConfig();
|
var config = MakeConfig();
|
||||||
var provider = new ZitadelAuthProvider(
|
var provider = new ZitadelAuthProvider(
|
||||||
factory, store, config, NullLogger<ZitadelAuthProvider>.Instance);
|
factory, new OnlineRefreshTokenStore(store), config, NullLogger<ZitadelAuthProvider>.Instance);
|
||||||
return (provider, handler, store);
|
return (provider, handler, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using ClaudeDo.Worker;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Worker.Tests.Tickets;
|
||||||
|
|
||||||
|
public sealed class DpapiTokenStoreTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _dir = Path.Combine(Path.GetTempPath(), "cdo-tok-" + Guid.NewGuid().ToString("N"));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Save_and_read_round_trip()
|
||||||
|
{
|
||||||
|
var store = new DpapiTokenStore(Path.Combine(_dir, "ticket.pat"));
|
||||||
|
store.Save("tsp_secret");
|
||||||
|
Assert.Equal("tsp_secret", store.Read());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Read_returns_null_when_missing()
|
||||||
|
=> Assert.Null(new DpapiTokenStore(Path.Combine(_dir, "nope.pat")).Read());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Two_stores_with_different_files_do_not_share_state()
|
||||||
|
{
|
||||||
|
var a = new DpapiTokenStore(Path.Combine(_dir, "a.token"));
|
||||||
|
var b = new DpapiTokenStore(Path.Combine(_dir, "b.token"));
|
||||||
|
a.Save("aaa");
|
||||||
|
b.Save("bbb");
|
||||||
|
Assert.Equal("aaa", a.Read());
|
||||||
|
Assert.Equal("bbb", b.Read());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Clear_removes_the_file()
|
||||||
|
{
|
||||||
|
var store = new DpapiTokenStore(Path.Combine(_dir, "c.token"));
|
||||||
|
store.Save("x");
|
||||||
|
store.Clear();
|
||||||
|
Assert.Null(store.Read());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_dir)) Directory.Delete(_dir, recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user