Files
OpenNest/OpenNest.Tests/Data/RemoteDrawingSetStoreTests.cs
T
ajandClaude Opus 5.5 28db6ae807 feat(server): release several drawing sets at once, merge into drafts, and undo
A release can now cover several sets, each with its own kit count, so parts
from different kits share sheets. It can also merge into an existing draft
nest instead of always creating one. Schema v9 replaces the single-set
release tables: releases, their set lines, their nests (created or merged,
with the nest revision before and after) and the drawing quantities each set
added to each nest, so a kit stays traceable once kits share a nest. The v8
migration turns each existing release into one set line with created nests.

Undo runs on the server in one transaction: created nests are soft-deleted
and merged nests restored to their pre-merge revision, but only while each
still holds what the release wrote. Otherwise nothing changes and the
changed nests are listed, so the operator can skip them. A nest restored to
that revision counts as unchanged, which lets releases sharing a nest be
undone newest first.

The old /api/sets/{id}/releases and /api/nests/{id}/release routes are
replaced by /api/releases; no tagged desktop used them. The desktop still
releases one set at a time until the release dialog is reworked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 10:25:52 -04:00

124 lines
5.0 KiB
C#

using System.Net;
using OpenNest.Data;
namespace OpenNest.Tests.Data;
public class RemoteDrawingSetStoreTests
{
private static RemoteDrawingSetStore Store(StubHandler handler) =>
new(new HttpClient(handler), "http://server:8090/base/");
private static DrawingSetRecord Set(Guid? drawing = null) => new()
{
Id = Guid.NewGuid(),
Name = "Synthetic kit",
Members = new[] { new DrawingSetMember { DrawingId = drawing ?? Guid.NewGuid(), Quantity = 2 } },
};
[Fact]
public async Task QueryAsync_SendsEscapedFilters()
{
var handler = new StubHandler(_ => StubHandler.Json(new { items = Array.Empty<object>(), total = 0, offset = 5, limit = 5 }));
using var store = Store(handler);
await store.QueryAsync(new DrawingSetQuery
{
Search = " 50%_ ",
Customer = "A&B",
IncludeArchived = true,
Sort = DrawingSetSortField.Customer,
Descending = false,
Offset = 5,
Limit = 5,
});
Assert.Equal(
"http://server:8090/base/api/sets/query?search=50%25_&sort=customer&order=asc&offset=5&limit=5&customer=A%26B&includeArchived=true",
handler.Requests.Single().RequestUri!.AbsoluteUri);
}
[Fact]
public async Task OldServer_RaisesServerTooOld()
{
using var store = Store(new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.NotFound)));
var query = await Assert.ThrowsAsync<ServerTooOldException>(() => store.QueryAsync(new DrawingSetQuery()));
await Assert.ThrowsAsync<ServerTooOldException>(() => store.GetAsync(Guid.NewGuid()));
var releases = await Assert.ThrowsAsync<ServerTooOldException>(() => store.QueryReleasesAsync(nestId: Guid.NewGuid()));
await Assert.ThrowsAsync<ServerTooOldException>(() => store.GetReleaseAsync(Guid.NewGuid()));
Assert.Contains("several drawing sets", releases.Message);
Assert.Contains("drawing sets", query.Message);
}
[Fact]
public async Task UnknownSet_OnASetAwareServer_IsNull()
{
var handler = new StubHandler(request => request.RequestUri!.AbsolutePath.EndsWith("/api/sets/query")
? StubHandler.Json(new { items = Array.Empty<object>(), total = 0, offset = 0, limit = 1 })
: new HttpResponseMessage(HttpStatusCode.NotFound));
using var store = Store(handler);
Assert.Null(await store.GetAsync(Guid.NewGuid()));
Assert.EndsWith("api/sets/query?limit=1", handler.Requests[1].RequestUri!.AbsoluteUri);
}
[Fact]
public async Task InvalidSet_IsRefusedBeforeSending()
{
var handler = new StubHandler(_ => throw new InvalidOperationException("Nothing may be sent."));
using var store = Store(handler);
var duplicate = Set();
duplicate.Members = duplicate.Members.Concat(duplicate.Members).ToList();
await Assert.ThrowsAsync<ArgumentException>(() => store.CreateAsync(duplicate));
await Assert.ThrowsAsync<ArgumentException>(() => store.PostReleaseAsync(new SetReleaseRecord
{
Id = Guid.NewGuid(),
Sets = new[] { new SetReleaseLine { SetId = Guid.NewGuid(), SetRevision = 1, Quantity = 0 } },
}));
await Assert.ThrowsAsync<ArgumentException>(() => store.UndoReleaseAsync(Guid.NewGuid(),
new SetReleaseUndoRequest { SkipNests = new[] { Guid.Empty } }));
Assert.Empty(handler.Requests);
}
[Fact]
public async Task CreateAsync_SendsMembersWithoutJoinedDrawingsAndChecksTheReply()
{
var drawing = Guid.NewGuid();
var set = Set(drawing);
set.Members[0].Drawing = new DrawingLibraryRecord { Id = drawing, Name = "Synthetic joined" };
var handler = new StubHandler(_ => StubHandler.Json(new
{
id = set.Id,
revision = 1,
name = set.Name,
members = new[] { new { drawingId = drawing, quantity = 3 } },
}));
using var store = Store(handler);
await Assert.ThrowsAsync<IOException>(() => store.CreateAsync(set));
Assert.DoesNotContain("Synthetic joined", handler.RequestBodies.Single());
}
[Fact]
public async Task PostReleaseAsync_ReplyForOtherNests_IsRefused()
{
var setId = Guid.NewGuid();
var release = new SetReleaseRecord
{
Id = Guid.NewGuid(),
Sets = new[] { new SetReleaseLine { SetId = setId, SetRevision = 1, Quantity = 1 } },
Nests = new[] { new SetReleaseNest { NestId = Guid.NewGuid(), RevisionAfter = 1, MaterialName = "Synthetic Steel" } },
};
var handler = new StubHandler(_ => StubHandler.Json(new
{
id = release.Id,
sets = new[] { new { setId, setRevision = 1, quantity = 1 } },
nests = new[] { new { nestId = Guid.NewGuid(), kind = "created", revisionAfter = 1 } },
}));
using var store = Store(handler);
await Assert.ThrowsAsync<IOException>(() => store.PostReleaseAsync(release));
}
}