zcbot/windows-node/Zcbot.WindowsNode.Tests/NodeApplicationControllerTe...

175 lines
5.6 KiB
C#

namespace Zcbot.WindowsNode.Tests;
public sealed class NodeApplicationControllerTests : IDisposable
{
private readonly string root = Path.Combine(
Path.GetTempPath(), "zcbot-windows-node-tests", Guid.NewGuid().ToString("N"));
[Fact]
public void StartWithoutIdentityRemainsNotRegistered()
{
var store = new FakeConfigStore();
var connections = new List<FakeConnection>();
using var controller = CreateController(store, connections);
controller.Start();
Assert.Equal(NodeState.NotRegistered, controller.CurrentStatus.State);
Assert.Null(controller.CurrentConfig);
Assert.Empty(connections);
}
[Fact]
public async Task ReconnectWaitsForOldSessionAndStartsOneReplacement()
{
var store = new FakeConfigStore { Config = CreateConfig() };
var connections = new List<FakeConnection>();
using var controller = CreateController(store, connections);
controller.Start();
Assert.Single(connections);
var reconnected = await controller.ReconnectAsync();
Assert.True(reconnected);
Assert.Equal(2, connections.Count);
Assert.True(connections[0].Completed);
Assert.False(connections[1].Completed);
await controller.StopAsync(NodeShutdownMode.WaitForJobs);
}
[Fact]
public async Task CancelStopIsIdempotentAndCancelsJobsOnce()
{
var store = new FakeConfigStore { Config = CreateConfig() };
var connections = new List<FakeConnection>();
using var controller = CreateController(store, connections);
controller.Start();
var first = controller.StopAsync(NodeShutdownMode.CancelJobs);
var second = controller.StopAsync(NodeShutdownMode.WaitForJobs);
await Task.WhenAll(first, second);
Assert.Same(first, second);
Assert.Equal(1, connections[0].CancelJobsCalls);
Assert.True(connections[0].Completed);
Assert.Equal(NodeState.Stopped, controller.CurrentStatus.State);
}
[Fact]
public async Task FailedMigrationRestartsTheExistingConfiguration()
{
var store = new FakeConfigStore { Config = CreateConfig() };
var connections = new List<FakeConnection>();
using var controller = CreateController(
store,
connections,
migrate: (_, _, _) => throw new IOException("copy failed"));
controller.Start();
await Assert.ThrowsAsync<IOException>(
() => controller.MigrateDataRootAsync(Path.Combine(root, "new-root")));
Assert.Equal(2, connections.Count);
Assert.True(connections[0].Completed);
Assert.False(connections[1].Completed);
await controller.StopAsync(NodeShutdownMode.WaitForJobs);
}
[Fact]
public async Task ResetIdentityWaitsForSessionBeforeDeletingIdentity()
{
var store = new FakeConfigStore { Config = CreateConfig() };
var connections = new List<FakeConnection>();
using var controller = CreateController(store, connections);
controller.Start();
await controller.ResetIdentityAsync();
Assert.True(connections[0].Completed);
Assert.True(store.IdentityDeleted);
Assert.Null(controller.CurrentConfig);
Assert.Equal(NodeState.NotRegistered, controller.CurrentStatus.State);
}
public void Dispose()
{
if (Directory.Exists(root))
{
Directory.Delete(root, recursive: true);
}
}
private NodeApplicationController CreateController(
FakeConfigStore store,
List<FakeConnection> connections,
Func<string, string, CancellationToken, Task<NodeDataMigrationResult>>? migrate = null)
{
var paths = new NodePaths(
root,
Path.Combine(root, "node.json"),
Path.Combine(root, "jobs"),
Path.Combine(root, "workspaces"));
return new NodeApplicationController(
store,
paths,
(_, _) =>
{
var connection = new FakeConnection();
connections.Add(connection);
return connection;
},
(_, _, _) => Task.CompletedTask,
migrate ?? ((source, target, _) => Task.FromResult(
new NodeDataMigrationResult(source, target, 0, 0))),
_ => { });
}
private static NodeConfig CreateConfig() => new(
new Uri("https://zcbot.example/"),
Guid.NewGuid(),
Guid.NewGuid(),
"test-node",
"secret",
30,
[]);
private sealed class FakeConfigStore : INodeConfigStore
{
public NodeConfig? Config { get; set; }
public bool IdentityDeleted { get; private set; }
public bool Exists => Config is not null;
public string ConfigPath => "node.json";
public void Save(NodeConfig config) => Config = config;
public NodeConfig Load() => Config
?? throw new NodeConfigurationException("missing identity");
public void DeleteLocalIdentity()
{
IdentityDeleted = true;
Config = null;
}
}
private sealed class FakeConnection : INodeConnection
{
public int CancelJobsCalls { get; private set; }
public bool Completed { get; private set; }
public async Task RunAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
finally
{
Completed = true;
}
}
public void CancelActiveJobsForExit() => CancelJobsCalls++;
}
}