mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-28 11:31:08 -04:00
Nests named offline from the per-PC counter can collide with names already on the server. Renaming them silently is not an option because the name may already be on posted CNC programs and labels, so the first Save of a nest to a server now checks for an active nest with exactly the same name and, only then, offers a freshly allocated name in an editable dialog (Cancel saves nothing). A failed or slow check keeps the current name, as before. Save As and pending-upload reconciliation are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
6.2 KiB
C#
146 lines
6.2 KiB
C#
using OpenNest.Data;
|
|
|
|
namespace OpenNest.Tests.Data;
|
|
|
|
public class NestSaveSessionTests
|
|
{
|
|
[Fact]
|
|
public async Task Save_CreatesThenUpdatesAndSaveCopyCreatesNewRecord()
|
|
{
|
|
var repository = new FakeRepository();
|
|
var session = new NestSaveSession();
|
|
var nest = new Nest("Shop job");
|
|
var bytes = new byte[] { 1, 2, 3 };
|
|
|
|
await session.SaveAsync(repository, "http://shop:8090/", nest, bytes);
|
|
var firstId = session.RemoteId;
|
|
Assert.NotEqual(Guid.Empty, firstId);
|
|
Assert.Equal(3, repository.LastRecord!.FileSize);
|
|
Assert.Equal("Shop job", repository.LastRecord.Name);
|
|
Assert.Equal(1, repository.Uploads);
|
|
|
|
await session.SaveAsync(repository, "http://shop:8090", nest, bytes);
|
|
Assert.Equal(firstId, repository.UpdatedId);
|
|
Assert.Equal(1, repository.Updates);
|
|
Assert.Equal(1, repository.ExpectedRevision);
|
|
Assert.Equal(2, session.Revision);
|
|
|
|
await session.SaveAsync(repository, "http://shop:8090", nest, bytes, saveCopy: true);
|
|
Assert.NotEqual(firstId, session.RemoteId);
|
|
Assert.Equal(2, repository.Uploads);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task IsFirstSave_OnlyWhenNothingIsBoundOrPendingOnThatServer()
|
|
{
|
|
var repository = new FakeRepository();
|
|
var session = new NestSaveSession();
|
|
Assert.True(session.IsFirstSave("http://shop:8090"));
|
|
|
|
await session.SaveAsync(repository, "http://shop:8090", new Nest("Job"), [1]);
|
|
Assert.False(session.IsFirstSave("http://shop:8090/"));
|
|
Assert.True(session.IsFirstSave("http://other:8090"));
|
|
|
|
repository.Fail = true;
|
|
await Assert.ThrowsAsync<IOException>(() =>
|
|
session.SaveAsync(repository, "http://other:8090", new Nest("Job"), [1]));
|
|
Assert.False(session.IsFirstSave("http://other:8090"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FailedSaveAndServerSwitch_NeverOverwritePriorIdentity()
|
|
{
|
|
var repository = new FakeRepository();
|
|
var session = new NestSaveSession();
|
|
var oldId = Guid.NewGuid();
|
|
session.Bind(oldId, "http://old:8090");
|
|
repository.Fail = true;
|
|
|
|
await Assert.ThrowsAsync<IOException>(() =>
|
|
session.SaveAsync(repository, "http://new:8090", new Nest("Job"), [1]));
|
|
Assert.Equal(oldId, session.RemoteId);
|
|
Assert.Equal("http://old:8090", session.ServerUrl);
|
|
Assert.NotEqual(Guid.Empty, repository.LastRecord!.Id);
|
|
Assert.Equal(session.PendingCreateId, repository.LastRecord.Id);
|
|
Assert.Equal(0, repository.Updates);
|
|
|
|
repository.Fail = false;
|
|
Assert.Null(await session.ReconcilePendingCreateAsync(repository, "http://new:8090"));
|
|
await session.SaveAsync(repository, "http://new:8090", new Nest("Job"), [1]);
|
|
Assert.NotEqual(oldId, session.RemoteId);
|
|
Assert.Equal("http://new:8090", session.ServerUrl);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task FailedUpdate_RetainsIdServerAndExpectedRevision_NoAutomaticRetry()
|
|
{
|
|
var repository = new FakeRepository { Fail = true };
|
|
var session = new NestSaveSession();
|
|
var id = Guid.NewGuid();
|
|
session.Bind(id, "http://shop/", 4);
|
|
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, "http://shop", new Nest("Job"), [1]));
|
|
Assert.Equal(id, session.RemoteId);
|
|
Assert.Equal("http://shop", session.ServerUrl);
|
|
Assert.Equal(4, session.Revision);
|
|
Assert.Equal(4, repository.ExpectedRevision);
|
|
Assert.Equal(1, repository.Updates);
|
|
Assert.True(session.RequiresReload);
|
|
await Assert.ThrowsAsync<IOException>(() => session.SaveAsync(repository, "http://shop", new Nest("Job"), [1]));
|
|
Assert.Equal(1, repository.Updates);
|
|
session.Bind(id, "http://shop", 5);
|
|
Assert.False(session.RequiresReload);
|
|
}
|
|
|
|
[Fact]
|
|
public void LegacyBind_DoesNotFabricateARevision()
|
|
{
|
|
var session = new NestSaveSession();
|
|
session.Bind(Guid.NewGuid(), "http://shop", 5);
|
|
session.Bind(Guid.NewGuid(), "http://shop");
|
|
Assert.Equal(0, session.Revision);
|
|
}
|
|
|
|
private sealed class FakeRepository : INestRepository
|
|
{
|
|
public int Uploads { get; private set; }
|
|
public int Updates { get; private set; }
|
|
public Guid UpdatedId { get; private set; }
|
|
public NestRecord? LastRecord { get; private set; }
|
|
public bool Fail { get; set; }
|
|
public int ExpectedRevision { get; private set; }
|
|
|
|
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default)
|
|
{
|
|
Uploads++;
|
|
LastRecord = record;
|
|
if (Fail) throw new IOException("Offline");
|
|
if (record.Id == Guid.Empty) record.Id = Guid.NewGuid();
|
|
record.Revision = 1;
|
|
return Task.FromResult(record);
|
|
}
|
|
|
|
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, int expectedRevision, CancellationToken ct = default)
|
|
{
|
|
ExpectedRevision = expectedRevision;
|
|
record.Revision = expectedRevision + 1;
|
|
return UpdateFileAsync(id, file, record, ct);
|
|
}
|
|
|
|
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, CancellationToken ct = default)
|
|
{
|
|
Updates++;
|
|
UpdatedId = id;
|
|
LastRecord = record;
|
|
if (Fail) throw new IOException("Offline");
|
|
return Task.FromResult(record);
|
|
}
|
|
|
|
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken ct = default) => throw new NotImplementedException();
|
|
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default) => throw new NotImplementedException();
|
|
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) => Task.FromResult<NestRecord?>(null);
|
|
public Task<byte[]?> GetFileAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
|
public Task<NestRecord> UpdateMetadataAsync(Guid id, NestRecord record, CancellationToken ct = default) => throw new NotImplementedException();
|
|
public Task DeleteAsync(Guid id, CancellationToken ct = default) => throw new NotImplementedException();
|
|
}
|
|
}
|