175 lines
5.6 KiB
C#
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++;
|
|
}
|
|
}
|