using System.Net; using System.Net.Http.Json; using Microsoft.Data.Sqlite; using OpenNest.Data; using static OpenNest.Server.Tests.DrawingLibraryFixtures; namespace OpenNest.Server.Tests; /// Content-addressed packages and structured nesting settings (schema v5). public sealed class DrawingLibraryStorageTests { private static readonly DrawingNestingSettings TestSquare = new() { CutFirst = true, Priority = 4, StepAngle = System.Math.PI / 2, StartAngle = 0, EndAngle = System.Math.PI, Allow180Equivalent = true, ColorArgb = unchecked((int)0xFF336699), }; [Fact] public async Task IdenticalPackages_AreStoredOnce() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var package = Package("shared"); var first = await Upload(client, Record("First"), package); var second = await Upload(client, Record("Second"), package); Assert.Equal(first.PackageSha256, second.PackageSha256); Assert.Equal(1L, Count(factory, "drawing_packages")); Assert.Equal(package, await client.GetByteArrayAsync($"/api/drawings/{second.Id}/file")); } [Fact] public async Task MetadataSettingsAndArchiveRevisions_AddNoPackage() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var record = Record(); var package = Package(); await Upload(client, record, package); record.Notes = "Edited notes"; var notes = await PutMetadata(client, record, 1); record.Settings = TestSquare; var settings = await PutMetadata(client, record, 2); using (var archive = await client.SendAsync(Guarded(HttpMethod.Post, $"/api/drawings/{record.Id}/archive", "\"3\""))) Assert.Equal(HttpStatusCode.OK, archive.StatusCode); Assert.Equal((2, 3), (notes.Revision, settings.Revision)); Assert.Equal(1L, Count(factory, "drawing_packages")); Assert.Equal(4L, Count(factory, "drawing_revisions")); for (var revision = 1; revision <= 4; revision++) Assert.Equal(package, await client.GetByteArrayAsync($"/api/drawings/{record.Id}/revisions/{revision}/file")); } [Fact] public async Task NewGeometry_AddsAPackage_AndEarlierRevisionsKeepTheirs() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var record = Record(); var original = Package("original"); var changed = Package("changed"); await Upload(client, record, original); using var response = await client.SendAsync(Guarded(HttpMethod.Put, $"/api/drawings/{record.Id}/file", "\"1\"", Body(record, changed))); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(2L, Count(factory, "drawing_packages")); Assert.Equal(original, await client.GetByteArrayAsync($"/api/drawings/{record.Id}/revisions/1/file")); Assert.Equal(changed, await client.GetByteArrayAsync($"/api/drawings/{record.Id}/revisions/2/file")); Assert.Equal(changed, await client.GetByteArrayAsync($"/api/drawings/{record.Id}/file")); } [Fact] public async Task Settings_RoundTripThroughEveryRead() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var record = Record(); record.Settings = TestSquare; var saved = await Upload(client, record, Package()); var current = await client.GetFromJsonAsync($"/api/drawings/{record.Id}", Options); var listed = Assert.Single((await client.GetFromJsonAsync("/api/drawings/query", Options))!.Items); var history = Assert.Single((await client.GetFromJsonAsync($"/api/drawings/{record.Id}/revisions", Options))!.Items); Assert.Equal(TestSquare, saved.Settings); Assert.Equal(TestSquare, current!.Settings); Assert.Equal(TestSquare, listed.Settings); Assert.Equal(TestSquare, history.Settings); } [Fact] public async Task SettingsOnlyEdit_IsARevision_AndRepeatingItIsANoOp() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var record = Record(); var saved = await Upload(client, record, Package()); record.Settings = TestSquare; var edited = await PutMetadata(client, record, 1); var repeated = await PutMetadata(client, record, 2); Assert.Equal(2, edited.Revision); Assert.Equal(TestSquare, edited.Settings); Assert.Equal(saved.PackageSha256, edited.PackageSha256); Assert.Equal(2, repeated.Revision); var history = (await client.GetFromJsonAsync($"/api/drawings/{record.Id}/revisions", Options))!; Assert.Equal(new[] { true, false }, history.Items.Select(r => r.Settings.CutFirst)); } [Fact] public async Task Query_FiltersByCutFirst() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var square = Record("Test square"); square.Settings = new DrawingNestingSettings { CutFirst = true }; await Upload(client, square, Package("square")); await Upload(client, Record("Bracket"), Package("bracket")); var cutFirst = (await client.GetFromJsonAsync("/api/drawings/query?cutFirst=true", Options))!; var others = (await client.GetFromJsonAsync("/api/drawings/query?cutFirst=false", Options))!; using var invalid = await client.GetAsync("/api/drawings/query?cutFirst=yes"); Assert.Equal("Test square", Assert.Single(cutFirst.Items).Name); Assert.Equal("Bracket", Assert.Single(others.Items).Name); Assert.Equal(HttpStatusCode.BadRequest, invalid.StatusCode); } [Theory] [InlineData("""{"name":"x","thickness":0,"units":"Inches","cutType":"Laser","settings":{"stepAngle":-1}}""")] [InlineData("""{"name":"x","thickness":0,"units":"Inches","cutType":"Laser","settings":null}""")] [InlineData("""{"name":"x","thickness":0,"units":"Inches","cutType":"Laser","settings":{"priority":"high"}}""")] public async Task InvalidSettings_AreRejectedWithNoState(string metadata) { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var id = Guid.NewGuid(); using var response = await client.PostAsync("/api/drawings", Body(metadata.Replace("{\"name\"", $"{{\"id\":\"{id}\",\"name\""), Package())); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal(0L, Count(factory, "drawing_packages")); Assert.Equal(0L, Count(factory, "drawings")); } private static async Task PutMetadata(HttpClient client, DrawingLibraryRecord record, int expected) { using var response = await client.SendAsync(Guarded(HttpMethod.Put, $"/api/drawings/{record.Id}/metadata", $"\"{expected}\"", JsonContent.Create(record, options: Options))); Assert.Equal(HttpStatusCode.OK, response.StatusCode); return (await response.Content.ReadFromJsonAsync(Options))!; } private static long Count(ServerFactory factory, string table) { using var connection = new SqliteConnection($"Data Source={factory.DatabasePath};Pooling=False"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = $"SELECT COUNT(*) FROM {table}"; return (long)command.ExecuteScalar()!; } }