mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 04:51:05 -04:00
feat(server): record set releases and the nests they made
Releasing a drawing set makes one nest per material group, so a kit run spans several nests. The server now records which set revision was released, the multiplier and the nests it made, so a run can be tracked across its nests and a nest can say where it came from. The client uploads every nest first and posts the release last: a failure part-way leaves ordinary nests and no release, never a release pointing at missing nests. For the same reason a release is accepted for an older set revision or an archived set, since the nests already exist by then. The release id is client-allocated and a nest belongs to at most one release, so a retried post gets 409 with the release that already holds it. Releases are append-only and read joined to each nest's current metadata, so status and deletion are always current. The tables join schema v7, as the set tables have not shipped yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
be057bf337
commit
44666b289d
14 files changed
+999
-46
No files matched your search
@@ -236,6 +236,7 @@ public sealed class DrawingLibraryDatabaseTests
|
||||
}
|
||||
|
||||
Execute(path, """
|
||||
DROP TABLE set_release_nests; DROP TABLE set_releases;
|
||||
DROP TABLE drawing_set_members; DROP TABLE drawing_sets; DROP TABLE drawing_set_revisions;
|
||||
DROP TABLE drawing_revisions; DROP TABLE drawings; DROP TABLE drawing_packages; PRAGMA user_version=3;
|
||||
""");
|
||||
|
||||
@@ -43,7 +43,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
Assert.Throws<SqliteException>(() => new NestDatabase(factory.DatabasePath, pooling: false));
|
||||
|
||||
Assert.Equal(6L, Scalar(factory.DatabasePath, "PRAGMA user_version"));
|
||||
Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'drawing_set%'"));
|
||||
Assert.Equal(0L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM sqlite_master WHERE name LIKE 'drawing_set%' OR name LIKE 'set_release%'"));
|
||||
Assert.Equal(before, Digest(factory.DatabasePath));
|
||||
Execute(factory.DatabasePath, "DROP TABLE ix_drawing_set_members_drawing");
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
@@ -295,7 +295,7 @@ public sealed class DrawingSetDatabaseTests
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A real schema-v6 file with nests and a library drawing: the set tables are additive,
|
||||
/// A real schema-v6 file with nests and a library drawing: the set and release tables are additive,
|
||||
/// so dropping them restores v6 exactly. Returns the drawing's id.
|
||||
/// </summary>
|
||||
private static Guid CreateVersion6Database(string path)
|
||||
@@ -314,7 +314,10 @@ public sealed class DrawingSetDatabaseTests
|
||||
database.UpdateDrawing(drawing, edit, null, 1);
|
||||
}
|
||||
|
||||
Execute(path, "DROP TABLE drawing_set_members; DROP TABLE drawing_sets; DROP TABLE drawing_set_revisions; PRAGMA user_version=6;");
|
||||
Execute(path, """
|
||||
DROP TABLE set_release_nests; DROP TABLE set_releases;
|
||||
DROP TABLE drawing_set_members; DROP TABLE drawing_sets; DROP TABLE drawing_set_revisions; PRAGMA user_version=6;
|
||||
""");
|
||||
return drawing;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,4 +50,40 @@ internal static class DrawingSetFixtures
|
||||
|
||||
public static IEnumerable<(Guid, int)> MembersOf(DrawingSetRecord record) =>
|
||||
record.Members.Select(m => (m.DrawingId, m.Quantity));
|
||||
|
||||
/// <summary>Stores a draft nest directly and returns its id.</summary>
|
||||
public static Guid AddNest(NestDatabase database, string name = "Synthetic nest") =>
|
||||
database.Insert(Guid.NewGuid(), new NestRecord { Name = name, Status = NestStatus.Draft }, new byte[] { 1, 2, 3 }).Id;
|
||||
|
||||
public static async Task<Guid> AddNest(HttpClient client, string name = "Synthetic nest", Guid? id = null)
|
||||
{
|
||||
var record = new NestRecord { Id = id ?? Guid.NewGuid(), Name = name, Status = NestStatus.Draft };
|
||||
using var body = new MultipartFormDataContent
|
||||
{
|
||||
{ new StringContent(JsonSerializer.Serialize(record, Options), Encoding.UTF8, "application/json"), "metadata" },
|
||||
{ new ByteArrayContent(new byte[] { 1, 2, 3 }), "file", "synthetic.nest" },
|
||||
};
|
||||
using var response = await client.PostAsync("/api/nests", body);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return record.Id;
|
||||
}
|
||||
|
||||
/// <summary>A release of <paramref name="setRevision"/> making one nest per id, each in its own material.</summary>
|
||||
public static SetReleaseRecord Release(int setRevision, params Guid[] nests) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SetRevision = setRevision,
|
||||
Multiplier = 2,
|
||||
Notes = "Synthetic release",
|
||||
ReleasedBy = "Synthetic operator",
|
||||
Nests = nests.Select((nest, i) => new SetReleaseNest
|
||||
{
|
||||
NestId = nest,
|
||||
MaterialName = "Synthetic Steel " + i,
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = 0.25,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Laser,
|
||||
}).ToList(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using OpenNest.Data;
|
||||
using static OpenNest.Server.Tests.DrawingLibraryFixtures;
|
||||
using static OpenNest.Server.Tests.DrawingSetFixtures;
|
||||
|
||||
namespace OpenNest.Server.Tests;
|
||||
|
||||
public sealed class SetReleaseTests
|
||||
{
|
||||
[Fact]
|
||||
public void Release_OfAStaleRevision_IsStoredAndJoinsItsSetNameAndNests()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var drawing = AddDrawing(database);
|
||||
var set = Set("Kit as released", members: (drawing, 1));
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
database.UpdateDrawingSet(set.Id, Set("Kit renamed later", set.Id, (drawing, 3)), 1);
|
||||
var first = AddNest(database, "Nest one");
|
||||
var second = AddNest(database, "Nest two");
|
||||
var release = Release(1, first, second);
|
||||
release.Nests[1].Thickness = 0.123456;
|
||||
release.Nests[1].Units = Units.Millimeters;
|
||||
release.Nests[1].CutType = CutType.Waterjet;
|
||||
|
||||
var stored = database.InsertSetRelease(set.Id, release)!;
|
||||
|
||||
Assert.Equal((release.Id, set.Id, 1, "Kit as released", 2), (stored.Id, stored.SetId, stored.SetRevision, stored.SetName, stored.Multiplier));
|
||||
Assert.Equal(("Synthetic release", "Synthetic operator"), (stored.Notes, stored.ReleasedBy));
|
||||
Assert.NotEqual(default, stored.ReleasedAt);
|
||||
Assert.Equal(new[] { first, second }, stored.Nests.Select(n => n.NestId));
|
||||
Assert.Equal(("Synthetic Steel 1", "Grade A", 0.1235, Units.Millimeters, CutType.Waterjet),
|
||||
(stored.Nests[1].MaterialName, stored.Nests[1].MaterialGrade, stored.Nests[1].Thickness, stored.Nests[1].Units, stored.Nests[1].CutType));
|
||||
Assert.Equal(("Nest one", NestStatus.Draft, false), (stored.Nests[0].Nest!.Name, stored.Nests[0].Nest!.Status, stored.Nests[0].Nest!.Deleted));
|
||||
Assert.Equal(release.Id, database.ReleaseForNest(second)!.Id);
|
||||
Assert.Null(database.ReleaseForNest(AddNest(database)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JoinedNests_FollowStatusChangesAndDeletion()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var set = Set();
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
var cut = AddNest(database, "Cut nest");
|
||||
var removed = AddNest(database, "Removed nest");
|
||||
database.InsertSetRelease(set.Id, Release(1, cut, removed));
|
||||
|
||||
var record = database.Get(cut)!;
|
||||
record.Status = NestStatus.HasBeenCut;
|
||||
database.UpdateMetadata(cut, record, 1);
|
||||
database.SetDeleted(removed, true, 1);
|
||||
|
||||
var nests = database.SetReleases(set.Id, 0, 100)!.Items.Single().Nests;
|
||||
Assert.Equal((NestStatus.HasBeenCut, false), (nests[0].Nest!.Status, nests[0].Nest!.Deleted));
|
||||
Assert.True(nests[1].Nest!.Deleted);
|
||||
Assert.True(database.ReleaseForNest(removed)!.Nests[1].Nest!.Deleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Releases_PageNewestFirstPerSetAndAcceptAnArchivedSet()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var set = Set();
|
||||
var other = Set("Other kit");
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
database.InsertDrawingSet(other.Id, other);
|
||||
var ids = new List<Guid>();
|
||||
for (var i = 0; i < 3; i++)
|
||||
{
|
||||
var release = Release(1, AddNest(database));
|
||||
ids.Add(release.Id);
|
||||
database.InsertSetRelease(set.Id, release);
|
||||
Thread.Sleep(5);
|
||||
}
|
||||
database.InsertSetRelease(other.Id, Release(1, AddNest(database)));
|
||||
database.SetDrawingSetArchived(set.Id, true, 1);
|
||||
var afterArchive = Release(1, AddNest(database));
|
||||
Assert.NotNull(database.InsertSetRelease(set.Id, afterArchive));
|
||||
ids.Add(afterArchive.Id);
|
||||
|
||||
var page = database.SetReleases(set.Id, 1, 2)!;
|
||||
Assert.Equal(4, page.Total);
|
||||
Assert.Equal(new[] { ids[2], ids[1] }, page.Items.Select(r => r.Id));
|
||||
Assert.Single(database.SetReleases(other.Id, 0, 100)!.Items);
|
||||
Assert.Null(database.SetReleases(Guid.NewGuid(), 0, 100));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => database.SetReleases(set.Id, 0, 201));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Refusals_LeaveNoState()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var set = Set();
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
var free = AddNest(database);
|
||||
var taken = AddNest(database);
|
||||
var deleted = AddNest(database);
|
||||
database.SetDeleted(deleted, true, 1);
|
||||
var owner = Release(1, taken);
|
||||
database.InsertSetRelease(set.Id, owner);
|
||||
|
||||
Assert.Null(database.InsertSetRelease(Guid.NewGuid(), Release(1, free)));
|
||||
Assert.Contains("no revision 2", Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, Release(2, free))).Message);
|
||||
Assert.Contains("does not exist", Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, Release(1, free, Guid.NewGuid()))).Message);
|
||||
Assert.Contains("is deleted", Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, Release(1, free, deleted))).Message);
|
||||
Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, Release(1, free, free)));
|
||||
Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, Release(1)));
|
||||
var noId = Release(1, free);
|
||||
noId.Id = Guid.Empty;
|
||||
Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, noId));
|
||||
var tooMany = Release(1, free);
|
||||
tooMany.Multiplier = SetReleaseRules.MaxMultiplier + 1;
|
||||
Assert.Throws<ArgumentException>(() => database.InsertSetRelease(set.Id, tooMany));
|
||||
|
||||
var claimed = Assert.Throws<SetReleaseConflictException>(() => database.InsertSetRelease(set.Id, Release(1, free, taken)));
|
||||
Assert.Equal(owner.Id, claimed.Current.Id);
|
||||
var reused = Release(1, free);
|
||||
reused.Id = owner.Id;
|
||||
Assert.Equal(owner.Id, Assert.Throws<SetReleaseConflictException>(() => database.InsertSetRelease(set.Id, reused)).Current.Id);
|
||||
|
||||
Assert.Equal(1L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM set_releases"));
|
||||
Assert.Equal(1L, Scalar(factory.DatabasePath, "SELECT COUNT(*) FROM set_release_nests"));
|
||||
Assert.Null(database.ReleaseForNest(free));
|
||||
}
|
||||
|
||||
// Code-level ownership gate, as for the set operations.
|
||||
[Theory]
|
||||
[InlineData("InsertSetRelease")]
|
||||
[InlineData("SetReleases")]
|
||||
[InlineData("ReleaseForNest")]
|
||||
public async Task Concurrency_EveryReleaseOperationWaitsForTheSameMonitor(string operation)
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var set = Set();
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
var nest = AddNest(database);
|
||||
var next = AddNest(database);
|
||||
database.InsertSetRelease(set.Id, Release(1, nest));
|
||||
var sync = typeof(NestDatabase).GetField("_sync", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(database)!;
|
||||
var completed = new TaskCompletionSource<Exception?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
var worker = new Thread(() => completed.SetResult(Xunit.Record.Exception(() =>
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case "InsertSetRelease": database.InsertSetRelease(set.Id, Release(1, next)); break;
|
||||
case "SetReleases": database.SetReleases(set.Id, 0, 100); break;
|
||||
case "ReleaseForNest": database.ReleaseForNest(nest); break;
|
||||
default: throw new InvalidOperationException($"Unknown test operation: {operation}");
|
||||
}
|
||||
})))
|
||||
{ IsBackground = true };
|
||||
|
||||
bool observedWaiting;
|
||||
bool completedWhileHeld;
|
||||
lock (sync)
|
||||
{
|
||||
worker.Start();
|
||||
observedWaiting = SpinWait.SpinUntil(() => completed.Task.IsCompleted ||
|
||||
(worker.ThreadState & ThreadState.WaitSleepJoin) != 0, TimeSpan.FromSeconds(10));
|
||||
completedWhileHeld = completed.Task.IsCompleted;
|
||||
}
|
||||
|
||||
Assert.True(worker.Join(TimeSpan.FromSeconds(10)), "Operation did not finish after releasing the monitor.");
|
||||
Assert.True(observedWaiting, "Caller never reached the operation within the watchdog.");
|
||||
Assert.False(completedWhileHeld, operation + " bypassed the shared monitor.");
|
||||
Assert.Null(await completed.Task);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Concurrency_AllReleaseSqlStepsOwnTheMonitor()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var database = new NestDatabase(factory.DatabasePath, pooling: false);
|
||||
var set = Set();
|
||||
database.InsertDrawingSet(set.Id, set);
|
||||
var nest = AddNest(database);
|
||||
var sync = typeof(NestDatabase).GetField("_sync", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(database)!;
|
||||
var connection = (SqliteConnection)typeof(NestDatabase)
|
||||
.GetField("_connection", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(database)!;
|
||||
var steps = 0;
|
||||
var unownedSteps = 0;
|
||||
SQLitePCL.raw.sqlite3_progress_handler(connection.Handle, 1, _ =>
|
||||
{
|
||||
steps++;
|
||||
if (!Monitor.IsEntered(sync))
|
||||
unownedSteps++;
|
||||
return 0;
|
||||
}, null);
|
||||
try
|
||||
{
|
||||
Action[] operations =
|
||||
[
|
||||
() => database.InsertSetRelease(set.Id, Release(1, nest)),
|
||||
() => database.SetReleases(set.Id, 0, 100),
|
||||
() => database.ReleaseForNest(nest),
|
||||
];
|
||||
foreach (var operation in operations)
|
||||
{
|
||||
var before = steps;
|
||||
operation();
|
||||
Assert.True(steps > before, "Operation did not execute the observed SQLite VM.");
|
||||
Assert.Equal(0, unownedSteps);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SQLitePCL.raw.sqlite3_progress_handler(connection.Handle, 0, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Api_PostsAndReadsReleasesWithCamelCaseEnums()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var client = factory.CreateClient();
|
||||
var set = await Create(client, Set());
|
||||
var first = await AddNest(client, "Nest one");
|
||||
var second = await AddNest(client, "Nest two");
|
||||
var release = Release(1, first, second);
|
||||
release.Nests[1].Units = Units.Millimeters;
|
||||
release.Nests[1].CutType = CutType.Plasma;
|
||||
|
||||
using var posted = await client.PostAsync($"/api/sets/{set.Id}/releases", Json(JsonSerializer.Serialize(release, Options)));
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, posted.StatusCode);
|
||||
var json = JsonNode.Parse(await posted.Content.ReadAsStringAsync())!;
|
||||
Assert.Equal(set.Id.ToString(), json["setId"]!.GetValue<string>());
|
||||
Assert.Equal("Synthetic kit", json["setName"]!.GetValue<string>());
|
||||
Assert.Equal("millimeters", json["nests"]![1]!["units"]!.GetValue<string>());
|
||||
Assert.Equal("plasma", json["nests"]![1]!["cutType"]!.GetValue<string>());
|
||||
Assert.Equal("draft", json["nests"]![0]!["nest"]!["status"]!.GetValue<string>());
|
||||
|
||||
var page = (await client.GetFromJsonAsync<SetReleasePage>($"/api/sets/{set.Id}/releases", Options))!;
|
||||
Assert.Equal(release.Id, page.Items.Single().Id);
|
||||
Assert.Equal("Nest two", page.Items.Single().Nests[1].Nest!.Name);
|
||||
var fromNest = (await client.GetFromJsonAsync<SetReleaseRecord>($"/api/nests/{second}/release", Options))!;
|
||||
Assert.Equal((release.Id, 2), (fromNest.Id, fromNest.Multiplier));
|
||||
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync($"/api/nests/{await AddNest(client)}/release")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync($"/api/sets/{Guid.NewGuid()}/releases")).StatusCode);
|
||||
Assert.Equal(HttpStatusCode.BadRequest, (await client.GetAsync($"/api/sets/{set.Id}/releases?limit=0")).StatusCode);
|
||||
using var unknownSet = await client.PostAsync($"/api/sets/{Guid.NewGuid()}/releases",
|
||||
Json(JsonSerializer.Serialize(Release(1, await AddNest(client)), Options)));
|
||||
Assert.Equal(HttpStatusCode.NotFound, unknownSet.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Api_Conflicts_ReturnTheReleaseHoldingTheIdOrNest()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var client = factory.CreateClient();
|
||||
var set = await Create(client, Set());
|
||||
var taken = await AddNest(client);
|
||||
var owner = Release(1, taken);
|
||||
using (await client.PostAsync($"/api/sets/{set.Id}/releases", Json(JsonSerializer.Serialize(owner, Options)))) { }
|
||||
|
||||
var reused = Release(1, await AddNest(client));
|
||||
reused.Id = owner.Id;
|
||||
foreach (var body in new[] { Release(1, await AddNest(client), taken), reused })
|
||||
{
|
||||
using var response = await client.PostAsync($"/api/sets/{set.Id}/releases", Json(JsonSerializer.Serialize(body, Options)));
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
Assert.Equal(owner.Id, (await response.Content.ReadFromJsonAsync<SetReleaseRecord>(Options))!.Id);
|
||||
}
|
||||
|
||||
Assert.Equal(1, (await client.GetFromJsonAsync<SetReleasePage>($"/api/sets/{set.Id}/releases", Options))!.Total);
|
||||
}
|
||||
|
||||
public static TheoryData<string, string> RejectedReleases()
|
||||
{
|
||||
var valid = JsonSerializer.Serialize(Release(1, NestA), Options);
|
||||
string With(string property, JsonNode? value)
|
||||
{
|
||||
var node = JsonNode.Parse(valid)!.AsObject();
|
||||
node[property] = value;
|
||||
return node.ToJsonString();
|
||||
}
|
||||
string WithNest(string property, JsonNode? value)
|
||||
{
|
||||
var node = JsonNode.Parse(valid)!.AsObject();
|
||||
node["nests"]![0]![property] = value;
|
||||
return node.ToJsonString();
|
||||
}
|
||||
string Without(string property)
|
||||
{
|
||||
var node = JsonNode.Parse(valid)!.AsObject();
|
||||
node.Remove(property);
|
||||
return node.ToJsonString();
|
||||
}
|
||||
|
||||
return new TheoryData<string, string>
|
||||
{
|
||||
{ "empty id", With("id", Guid.Empty.ToString()) },
|
||||
{ "other set id", With("setId", Guid.NewGuid().ToString()) },
|
||||
{ "zero set revision", With("setRevision", 0) },
|
||||
{ "unknown set revision", With("setRevision", 2) },
|
||||
{ "zero multiplier", With("multiplier", 0) },
|
||||
{ "multiplier over the limit", With("multiplier", 1001) },
|
||||
{ "long notes", With("notes", new string('x', 4001)) },
|
||||
{ "null released by", With("releasedBy", null) },
|
||||
{ "missing nests", Without("nests") },
|
||||
{ "null nests", With("nests", null) },
|
||||
{ "no nests", With("nests", new JsonArray()) },
|
||||
{ "repeated nest", With("nests", new JsonArray(JsonNode.Parse(valid)!["nests"]![0]!.DeepClone(), JsonNode.Parse(valid)!["nests"]![0]!.DeepClone())) },
|
||||
{ "unknown nest", WithNest("nestId", Guid.NewGuid().ToString()) },
|
||||
{ "empty nest id", WithNest("nestId", Guid.Empty.ToString()) },
|
||||
{ "integer units", WithNest("units", 0) },
|
||||
{ "unknown units", WithNest("units", "Feet") },
|
||||
{ "combined cut type", WithNest("cutType", "Laser, Plasma") },
|
||||
{ "integer cut type", WithNest("cutType", 1) },
|
||||
{ "negative thickness", WithNest("thickness", -1) },
|
||||
{ "null material", WithNest("materialName", null) },
|
||||
{ "long grade", WithNest("materialGrade", new string('g', 201)) },
|
||||
{ "not an object", "[1]" },
|
||||
{ "malformed", "{" },
|
||||
};
|
||||
}
|
||||
|
||||
private static readonly Guid NestA = Guid.Parse("00000000-0000-0000-0000-0000000000a1");
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(RejectedReleases))]
|
||||
public async Task Api_InvalidRelease_IsRejectedWithNoState(string row, string json)
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var client = factory.CreateClient();
|
||||
var set = await Create(client, Set());
|
||||
await AddNest(client, id: NestA);
|
||||
|
||||
using var response = await client.PostAsync($"/api/sets/{set.Id}/releases", Json(json));
|
||||
|
||||
Assert.True(response.StatusCode == HttpStatusCode.BadRequest, row + ": " + response.StatusCode);
|
||||
Assert.Equal(0, (await client.GetFromJsonAsync<SetReleasePage>($"/api/sets/{set.Id}/releases", Options))!.Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Api_JoinedNestInTheBody_IsIgnored()
|
||||
{
|
||||
using var factory = new ServerFactory();
|
||||
using var client = factory.CreateClient();
|
||||
var set = await Create(client, Set());
|
||||
var node = JsonNode.Parse(JsonSerializer.Serialize(Release(1, await AddNest(client)), Options))!;
|
||||
node["nests"]![0]!["nest"] = new JsonObject { ["status"] = 99, ["name"] = 5 };
|
||||
|
||||
using var response = await client.PostAsync($"/api/sets/{set.Id}/releases", Json(node.ToJsonString()));
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
}
|
||||
|
||||
private static object? Scalar(string path, string sql)
|
||||
{
|
||||
using var connection = new SqliteConnection($"Data Source={path};Pooling=False");
|
||||
connection.Open();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return command.ExecuteScalar();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user