mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-29 15:51:06 -04:00
A drawing every nest needs, such as the operator's test square cut before the job, had to be saved once per material and thickness or found with Show all materials. A blank material or grade, or a zero thickness, now means any: the server's library filter returns such drawings for every value, and adding one to a nest is not a difference, so it keeps its cut pins and takes the nest's material for the fields it left blank. Duplicate detection still compares every key exactly on the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
261 lines
12 KiB
C#
261 lines
12 KiB
C#
using System.Net;
|
|
using System.Net.Http.Json;
|
|
using OpenNest.Data;
|
|
using static OpenNest.Server.Tests.DrawingLibraryFixtures;
|
|
|
|
namespace OpenNest.Server.Tests;
|
|
|
|
public sealed class DrawingLibraryQueryTests
|
|
{
|
|
private static async Task<Dictionary<string, DrawingLibraryRecord>> SeedNamed(HttpClient client)
|
|
{
|
|
var rows = new (string Name, string Customer, string Material, string Grade, double Thickness, CutType CutType, string Notes)[]
|
|
{
|
|
("Alpha bracket", "Alpha Fabrication", "Carbon Steel", "A36 HR", 0.25, CutType.Laser, ""),
|
|
("Beta plate", "beta works", "carbon steel", " a36 hr", 0.1875, CutType.Waterjet, "Gamma 50% notes"),
|
|
("Delta_Under", "Gamma 50% Shop", "Stainless", "304", 0.25, CutType.Plasma, ""),
|
|
("Echo", "Alpha Fabrication", "Carbon Steel", "A36 HR", 0.25004, CutType.Laser, ""),
|
|
};
|
|
var saved = new Dictionary<string, DrawingLibraryRecord>();
|
|
foreach (var row in rows)
|
|
{
|
|
var record = Record(row.Name);
|
|
record.Customer = row.Customer;
|
|
record.MaterialName = row.Material;
|
|
record.MaterialGrade = row.Grade;
|
|
record.Thickness = row.Thickness;
|
|
record.CutType = row.CutType;
|
|
record.Notes = row.Notes;
|
|
saved[row.Name] = await Upload(client, record, Package(row.Name));
|
|
}
|
|
|
|
using var archived = await client.SendAsync(Guarded(HttpMethod.Post, $"/api/drawings/{saved["Echo"].Id}/archive", "\"1\""));
|
|
archived.EnsureSuccessStatusCode();
|
|
return saved;
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("", "Alpha bracket|Beta plate|Delta_Under")]
|
|
[InlineData("includeArchived=true", "Alpha bracket|Beta plate|Delta_Under|Echo")]
|
|
[InlineData("includeArchived=false", "Alpha bracket|Beta plate|Delta_Under")]
|
|
[InlineData("cutType=laser", "Alpha bracket")]
|
|
[InlineData("cutType=LASER&includeArchived=true", "Alpha bracket|Echo")]
|
|
[InlineData("cutType=Waterjet", "Beta plate")]
|
|
[InlineData("materialName=CARBON%20%20steel%20", "Alpha bracket|Beta plate")]
|
|
[InlineData("materialGrade=a36%20HR", "Alpha bracket|Beta plate")]
|
|
[InlineData("customer=ALPHA%20fabrication", "Alpha bracket")]
|
|
[InlineData("customer=Beta%20Works", "Beta plate")]
|
|
[InlineData("thickness=0.25", "Alpha bracket|Delta_Under")]
|
|
[InlineData("thickness=0.25&includeArchived=true", "Alpha bracket|Delta_Under|Echo")]
|
|
[InlineData("thickness=0.1875", "Beta plate")]
|
|
[InlineData("thickness=0.1874", "")]
|
|
[InlineData("thickness=0.25&materialName=Carbon%20Steel&cutType=laser", "Alpha bracket")]
|
|
[InlineData("materialName=Stainless&cutType=laser", "")]
|
|
[InlineData("customer=&materialName=", "Alpha bracket|Beta plate|Delta_Under")]
|
|
[InlineData("search=0%25", "Beta plate|Delta_Under")]
|
|
[InlineData("search=_", "Delta_Under")]
|
|
[InlineData("search=%5C", "")]
|
|
[InlineData("search=%20GAMMA%20", "Beta plate|Delta_Under")]
|
|
[InlineData("search=stainless", "")]
|
|
[InlineData("search=bracket&cutType=plasma", "")]
|
|
public async Task Filters_MatchExactKeysAndLiteralSearch(string query, string expected)
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
await SeedNamed(client);
|
|
|
|
var page = await DrawingLibraryApiTests.Query(client, query + "&sort=name&order=asc");
|
|
|
|
Assert.Equal(expected, string.Join("|", page.Items.Select(r => r.Name)));
|
|
Assert.Equal(page.Items.Count, page.Total);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("materialName=Carbon%20Steel", "Any|Any thickness|Steel")]
|
|
[InlineData("materialName=Carbon%20Steel&materialGrade=A36&thickness=0.25", "Any|Any thickness|Steel")]
|
|
[InlineData("materialName=Stainless&thickness=0.25", "Any")]
|
|
[InlineData("materialName=Carbon%20Steel&thickness=0.1875", "Any|Any thickness")]
|
|
[InlineData("thickness=0", "Any|Any thickness")]
|
|
public async Task UnsetMaterialGradeOrThickness_MatchesAnyFilter(string query, string expected)
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
foreach (var (name, material, grade, thickness) in new[]
|
|
{
|
|
("Any", "", "", 0.0),
|
|
("Any thickness", "Carbon Steel", "", 0.0),
|
|
("Steel", "Carbon Steel", "A36", 0.25),
|
|
})
|
|
{
|
|
var record = Record(name);
|
|
record.MaterialName = material;
|
|
record.MaterialGrade = grade;
|
|
record.Thickness = thickness;
|
|
await Upload(client, record, Package(name));
|
|
}
|
|
|
|
var page = await DrawingLibraryApiTests.Query(client, query + "&sort=name&order=asc");
|
|
|
|
Assert.Equal(expected, string.Join("|", page.Items.Select(r => r.Name)));
|
|
Assert.Equal(page.Items.Count, page.Total);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("name", false)]
|
|
[InlineData("name", true)]
|
|
[InlineData("customer", false)]
|
|
[InlineData("materialName", true)]
|
|
[InlineData("materialGrade", false)]
|
|
[InlineData("thickness", true)]
|
|
[InlineData("thickness", false)]
|
|
[InlineData("cutType", false)]
|
|
[InlineData("revision", true)]
|
|
public async Task Sort_MatchesOracleAndPagesPartitionMatches(string sort, bool descending)
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
string[] names = { "beta", "Alpha", "alpha", "BETA", "gamma" };
|
|
string[] grades = { "a36", "A36", "304" };
|
|
double[] thicknesses = { 0.25, 0.1875, 0.25 };
|
|
CutType[] cutTypes = { CutType.Plasma, CutType.Laser, CutType.Waterjet };
|
|
var saved = new List<DrawingLibraryRecord>();
|
|
for (var i = 0; i < 13; i++)
|
|
{
|
|
var record = Record(names[i % names.Length]);
|
|
record.Customer = names[(i + 2) % names.Length];
|
|
record.MaterialName = names[(i + 1) % names.Length];
|
|
record.MaterialGrade = grades[i % grades.Length];
|
|
record.Thickness = thicknesses[i % thicknesses.Length];
|
|
record.CutType = cutTypes[i % cutTypes.Length];
|
|
var uploaded = await Upload(client, record, Package(i.ToString()));
|
|
if (i % 4 == 0)
|
|
{
|
|
record.Notes = "edited";
|
|
using var edit = await client.SendAsync(Guarded(HttpMethod.Put, $"/api/drawings/{record.Id}/metadata", "\"1\"",
|
|
JsonContent.Create(record, options: Options)));
|
|
uploaded = (await edit.Content.ReadFromJsonAsync<DrawingLibraryRecord>(Options))!;
|
|
}
|
|
saved.Add(uploaded);
|
|
}
|
|
|
|
var expected = Oracle(saved, sort, descending).Select(r => r.Id).ToList();
|
|
var order = descending ? "desc" : "asc";
|
|
var full = await DrawingLibraryApiTests.Query(client, $"sort={sort}&order={order}");
|
|
var paged = new List<Guid>();
|
|
for (var offset = 0; offset < saved.Count; offset += 5)
|
|
paged.AddRange((await DrawingLibraryApiTests.Query(client, $"sort={sort}&order={order}&offset={offset}&limit=5")).Items.Select(r => r.Id));
|
|
|
|
Assert.Equal(expected, full.Items.Select(r => r.Id));
|
|
Assert.Equal(expected, paged);
|
|
Assert.Equal(saved.Count, full.Total);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DefaultOrder_IsNewestSavedFirst()
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
var first = await Upload(client, Record("First"), Package("1"));
|
|
var second = await Upload(client, Record("Second"), Package("2"));
|
|
|
|
var page = await DrawingLibraryApiTests.Query(client, "");
|
|
|
|
var expected = new[] { first, second }
|
|
.OrderByDescending(r => r.SavedAt.ToString("o"), StringComparer.Ordinal)
|
|
.ThenByDescending(r => r.Id.ToString(), StringComparer.Ordinal)
|
|
.Select(r => r.Id);
|
|
Assert.Equal(expected, page.Items.Select(r => r.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Limit_BoundsThePageAndTotalCountsEveryMatch()
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
for (var i = 0; i < 3; i++)
|
|
await Upload(client, Record($"Drawing {i}"), Package(i.ToString()));
|
|
|
|
var page = await DrawingLibraryApiTests.Query(client, "limit=2&offset=1");
|
|
var maximum = await DrawingLibraryApiTests.Query(client, "limit=500");
|
|
var beyond = await DrawingLibraryApiTests.Query(client, "offset=2147483647");
|
|
|
|
Assert.Equal(2, page.Items.Count);
|
|
Assert.Equal(3, page.Total);
|
|
Assert.Equal(1, page.Offset);
|
|
Assert.Equal(2, page.Limit);
|
|
Assert.Equal(3, maximum.Items.Count);
|
|
Assert.Empty(beyond.Items);
|
|
Assert.Equal(3, beyond.Total);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("unknown=1")]
|
|
[InlineData("limit=0")]
|
|
[InlineData("limit=501")]
|
|
[InlineData("limit=-1")]
|
|
[InlineData("offset=-1")]
|
|
[InlineData("offset=x")]
|
|
[InlineData("sort=1")]
|
|
[InlineData("sort=package")]
|
|
[InlineData("sort=name%20desc")]
|
|
[InlineData("order=up")]
|
|
[InlineData("cutType=1")]
|
|
[InlineData("cutType=Laser,Plasma")]
|
|
[InlineData("cutType=oxyfuel")]
|
|
[InlineData("cutType=")]
|
|
[InlineData("cutType=laser&cutType=plasma")]
|
|
[InlineData("thickness=-1")]
|
|
[InlineData("thickness=NaN")]
|
|
[InlineData("thickness=Infinity")]
|
|
[InlineData("thickness=1e3")]
|
|
[InlineData("thickness=")]
|
|
[InlineData("includeArchived=yes")]
|
|
[InlineData("search=a%00b")]
|
|
[InlineData("customer=a%00b")]
|
|
public async Task InvalidParameters_AreRejected(string query)
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
|
|
using var response = await client.GetAsync("/api/drawings/query?" + query);
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task OverlongSearchOrFilter_IsRejected()
|
|
{
|
|
using var factory = new ServerFactory();
|
|
using var client = factory.CreateClient();
|
|
|
|
using var search = await client.GetAsync("/api/drawings/query?search=" + new string('s', 201));
|
|
using var customer = await client.GetAsync("/api/drawings/query?customer=" + new string('c', 201));
|
|
using var boundary = await client.GetAsync("/api/drawings/query?search=" + new string('s', 200));
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, search.StatusCode);
|
|
Assert.Equal(HttpStatusCode.BadRequest, customer.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, boundary.StatusCode);
|
|
}
|
|
|
|
/// <summary>Independent oracle: ASCII case-folded text or numeric value, then id, in one direction.</summary>
|
|
private static IEnumerable<DrawingLibraryRecord> Oracle(List<DrawingLibraryRecord> records, string sort, bool descending)
|
|
{
|
|
Func<DrawingLibraryRecord, IComparable> key = sort switch
|
|
{
|
|
"name" => r => r.Name.ToUpperInvariant(),
|
|
"customer" => r => r.Customer.ToUpperInvariant(),
|
|
"materialName" => r => r.MaterialName.ToUpperInvariant(),
|
|
"materialGrade" => r => r.MaterialGrade.ToUpperInvariant(),
|
|
"thickness" => r => r.Thickness,
|
|
"cutType" => r => r.CutType.ToString(),
|
|
"revision" => r => r.Revision,
|
|
_ => throw new ArgumentOutOfRangeException(nameof(sort)),
|
|
};
|
|
var comparer = Comparer<IComparable>.Create((a, b) =>
|
|
a is string x && b is string y ? string.CompareOrdinal(x, y) : a.CompareTo(b));
|
|
return descending
|
|
? records.OrderByDescending(key, comparer).ThenByDescending(r => r.Id.ToString(), StringComparer.Ordinal)
|
|
: records.OrderBy(key, comparer).ThenBy(r => r.Id.ToString(), StringComparer.Ordinal);
|
|
}
|
|
}
|