mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-26 22:01:06 -04:00
feat(data): library insert model
Adding a library drawing to a nest has several outcomes that must be decided before anything changes, so the desktop can show warnings first and so every rule is tested on Linux. `LibraryDrawingInsert.Plan` returns a new exact copy carrying its library link, an added quantity on the copy already there, or a refusal that leaves the nest untouched. - Material, grade or thickness that differ from the nest still add the drawing but drop its cut pins, with one warning naming each difference (D3). Names compare by SharedListNames key and thickness at four decimals, matching the server's own filters. - Different units are refused: conversion is out of scope and a silent unit mismatch would scale the part. - The same revision from the same server only raises the quantity; a different revision of that drawing is refused in favour of Update Drawings (D2b). - Another drawing with the same name is refused naming both, because the name-keyed DrawingCollection would otherwise drop one silently. - A package whose hash or units disagree with its record is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15654c0690
commit
4dbb4527e2
3 files changed
+445
No files matched your search
@@ -0,0 +1,204 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using OpenNest;
|
||||
|
||||
namespace OpenNest.Data;
|
||||
|
||||
/// <summary>A downloaded library package as the caller read it (with <c>DrawingPackage.Read</c>).</summary>
|
||||
public sealed class LibraryPackage
|
||||
{
|
||||
public LibraryPackage(byte[] bytes, Drawing drawing, Units units, IReadOnlyList<string>? warnings = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bytes);
|
||||
ArgumentNullException.ThrowIfNull(drawing);
|
||||
Sha256 = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
|
||||
Drawing = drawing;
|
||||
Units = units;
|
||||
Warnings = warnings ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>Lower-case hex SHA-256 of the downloaded bytes.</summary>
|
||||
public string Sha256 { get; }
|
||||
|
||||
/// <summary>The package's drawing; the insert adopts this instance as the nest's copy.</summary>
|
||||
public Drawing Drawing { get; }
|
||||
|
||||
public Units Units { get; }
|
||||
|
||||
public IReadOnlyList<string> Warnings { get; }
|
||||
}
|
||||
|
||||
public enum LibraryInsertKind
|
||||
{
|
||||
/// <summary>The library drawing becomes a new nest drawing.</summary>
|
||||
AddDrawing,
|
||||
|
||||
/// <summary>The nest already holds this exact revision; only its quantity grows.</summary>
|
||||
AddQuantity,
|
||||
|
||||
/// <summary>Nothing may change; <see cref="LibraryDrawingInsert.Refusal"/> says why.</summary>
|
||||
Refused,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decides how one library drawing revision enters a nest, before anything changes:
|
||||
/// a new exact copy carrying its library link, more quantity on the copy already there,
|
||||
/// or a refusal. Material, grade or thickness that differ from the nest still allow the
|
||||
/// add, but the drawing's cut pins are dropped (D3). Different units, a different revision
|
||||
/// of the same drawing, or another drawing with the same name refuse it.
|
||||
/// </summary>
|
||||
public sealed class LibraryDrawingInsert
|
||||
{
|
||||
private bool _applied;
|
||||
|
||||
private LibraryDrawingInsert(Nest nest, LibraryInsertKind kind, Drawing? drawing, int quantity,
|
||||
bool keepPins, IReadOnlyList<string> warnings, string refusal)
|
||||
{
|
||||
Nest = nest;
|
||||
Kind = kind;
|
||||
Drawing = drawing;
|
||||
Quantity = quantity;
|
||||
KeepPins = keepPins;
|
||||
Warnings = warnings;
|
||||
Refusal = refusal;
|
||||
}
|
||||
|
||||
public Nest Nest { get; }
|
||||
|
||||
public LibraryInsertKind Kind { get; }
|
||||
|
||||
/// <summary>The new copy (<see cref="LibraryInsertKind.AddDrawing"/>) or the existing one; null when refused.</summary>
|
||||
public Drawing? Drawing { get; }
|
||||
|
||||
/// <summary>The quantity added to the nest's required count.</summary>
|
||||
public int Quantity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// For a new copy, true when the library drawing's cut pins may be used in this nest:
|
||||
/// the library material, grade and thickness match the nest's. An added quantity
|
||||
/// leaves the existing copy as it is.
|
||||
/// </summary>
|
||||
public bool KeepPins { get; }
|
||||
|
||||
/// <summary>Operator-facing notes to show before the add completes.</summary>
|
||||
public IReadOnlyList<string> Warnings { get; }
|
||||
|
||||
/// <summary>Why nothing may change; empty unless <see cref="Kind"/> is <see cref="LibraryInsertKind.Refused"/>.</summary>
|
||||
public string Refusal { get; }
|
||||
|
||||
public bool IsRefused => Kind == LibraryInsertKind.Refused;
|
||||
|
||||
public static LibraryDrawingInsert Plan(Nest nest, string serverUrl, DrawingLibraryRecord record,
|
||||
LibraryPackage package, int quantity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(nest);
|
||||
ArgumentNullException.ThrowIfNull(record);
|
||||
ArgumentNullException.ThrowIfNull(package);
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(quantity, 1);
|
||||
var serverKey = ServerUrl.Key(serverUrl);
|
||||
if (serverKey.Length == 0)
|
||||
throw new ArgumentException("A library insert needs the server the drawing came from.", nameof(serverUrl));
|
||||
|
||||
var label = Describe(record);
|
||||
if (!string.Equals(package.Sha256, record.PackageSha256, StringComparison.OrdinalIgnoreCase))
|
||||
return Refuse(nest, $"The downloaded package for {label} does not match the library record. Try again.");
|
||||
if (package.Units != record.Units)
|
||||
return Refuse(nest, $"The package for {label} is drawn in {UnitName(package.Units)}, "
|
||||
+ $"but its library record says {UnitName(record.Units)}.");
|
||||
if (package.Units != nest.Units)
|
||||
return Refuse(nest, $"{label} is drawn in {UnitName(package.Units)} and this nest is in "
|
||||
+ $"{UnitName(nest.Units)}. Converting units is not supported.");
|
||||
|
||||
var link = new DrawingLibraryLink(serverKey, record.Id, record.Revision, record.CutType,
|
||||
record.Thickness, record.MaterialName, record.MaterialGrade, record.Units, record.Notes,
|
||||
record.PackageSha256.ToLowerInvariant());
|
||||
|
||||
var sameRevision = nest.Drawings.FirstOrDefault(d => d.Library?.IsSameRevision(link) == true);
|
||||
if (sameRevision != null)
|
||||
return new LibraryDrawingInsert(nest, LibraryInsertKind.AddQuantity, sameRevision, quantity,
|
||||
keepPins: false, Array.Empty<string>(), "");
|
||||
|
||||
var otherRevision = nest.Drawings.FirstOrDefault(d => d.Library is { } l
|
||||
&& l.Id == link.Id && string.Equals(l.ServerKey, link.ServerKey, StringComparison.Ordinal));
|
||||
if (otherRevision != null)
|
||||
return Refuse(nest, $"This nest already uses revision {otherRevision.Library!.Revision} of this "
|
||||
+ $"library drawing as '{otherRevision.Name}'. Use Update Drawings to change its revision.");
|
||||
|
||||
// DrawingCollection is keyed by name, so a second drawing with this name would be lost.
|
||||
var sameName = nest.Drawings.FirstOrDefault(d => string.Equals(d.Name, record.Name, StringComparison.Ordinal));
|
||||
if (sameName != null)
|
||||
return Refuse(nest, $"This nest already has a drawing named '{sameName.Name}' "
|
||||
+ $"({DescribeExisting(sameName)}), so {label} was not added. Rename one of them first.");
|
||||
|
||||
var differences = Differences(nest, record);
|
||||
var warnings = package.Warnings.ToList();
|
||||
if (differences.Count > 0)
|
||||
warnings.Add($"{label} differs from this nest: {string.Join("; ", differences)}. The drawing is "
|
||||
+ "added, but its drawing cut pins are dropped and the nest's cutting settings apply.");
|
||||
|
||||
var drawing = package.Drawing;
|
||||
drawing.Name = record.Name;
|
||||
drawing.Customer = record.Customer;
|
||||
drawing.Material = new Material(record.MaterialName, record.MaterialGrade, drawing.Material?.Density ?? 0);
|
||||
drawing.Quantity.Required = quantity;
|
||||
drawing.Library = link;
|
||||
return new LibraryDrawingInsert(nest, LibraryInsertKind.AddDrawing, drawing, quantity,
|
||||
keepPins: differences.Count == 0, warnings, "");
|
||||
}
|
||||
|
||||
/// <summary>Changes the planned nest; call at most once, and only for an insert that is not refused.</summary>
|
||||
public void Apply()
|
||||
{
|
||||
if (IsRefused)
|
||||
throw new InvalidOperationException(Refusal);
|
||||
if (_applied)
|
||||
throw new InvalidOperationException("This library insert was already applied.");
|
||||
|
||||
if (Kind == LibraryInsertKind.AddQuantity)
|
||||
{
|
||||
if (!Nest.Drawings.Any(d => ReferenceEquals(d, Drawing)))
|
||||
throw new InvalidOperationException($"'{Drawing!.Name}' is no longer in this nest.");
|
||||
Drawing!.Quantity.Required += Quantity;
|
||||
}
|
||||
else if (!Nest.Drawings.Add(Drawing!))
|
||||
{
|
||||
throw new InvalidOperationException($"This nest already has a drawing named '{Drawing!.Name}'.");
|
||||
}
|
||||
|
||||
_applied = true;
|
||||
}
|
||||
|
||||
private static LibraryDrawingInsert Refuse(Nest nest, string reason) =>
|
||||
new(nest, LibraryInsertKind.Refused, null, 0, keepPins: false, Array.Empty<string>(), reason);
|
||||
|
||||
private static List<string> Differences(Nest nest, DrawingLibraryRecord record)
|
||||
{
|
||||
var differences = new List<string>();
|
||||
var material = nest.Material ?? new Material();
|
||||
if (SharedListNames.Key(record.MaterialName) != SharedListNames.Key(material.Name))
|
||||
differences.Add($"material {Text(record.MaterialName)} (nest {Text(material.Name)})");
|
||||
if (SharedListNames.Key(record.MaterialGrade) != SharedListNames.Key(material.Grade))
|
||||
differences.Add($"grade {Text(record.MaterialGrade)} (nest {Text(material.Grade)})");
|
||||
var thickness = DrawingLibraryRules.NormalizeThickness(record.Thickness);
|
||||
var nestThickness = DrawingLibraryRules.NormalizeThickness(nest.Thickness);
|
||||
if (thickness != nestThickness)
|
||||
differences.Add($"thickness {Number(thickness)} (nest {Number(nestThickness)})");
|
||||
return differences;
|
||||
}
|
||||
|
||||
private static string Describe(DrawingLibraryRecord record) =>
|
||||
string.IsNullOrWhiteSpace(record.Customer)
|
||||
? $"library drawing '{record.Name}' revision {record.Revision}"
|
||||
: $"library drawing '{record.Name}' ({record.Customer}) revision {record.Revision}";
|
||||
|
||||
private static string DescribeExisting(Drawing drawing) =>
|
||||
drawing.Library is { } link
|
||||
? $"library revision {link.Revision} from {link.ServerKey}"
|
||||
: "not from the drawing library";
|
||||
|
||||
private static string Text(string? value) => string.IsNullOrWhiteSpace(value) ? "not set" : value.Trim();
|
||||
|
||||
private static string Number(double value) => value.ToString("0.####", CultureInfo.CurrentCulture);
|
||||
|
||||
private static string UnitName(Units units) => units.ToString().ToLowerInvariant();
|
||||
}
|
||||
@@ -3,6 +3,12 @@ namespace OpenNest.Data;
|
||||
/// <summary>Base-address validation shared by the nest server's HTTP clients.</summary>
|
||||
internal static class ServerUrl
|
||||
{
|
||||
/// <summary>
|
||||
/// The key that binds saved nests and library links to a server: the address trimmed,
|
||||
/// without trailing slashes. Two settings that differ only that way name the same server.
|
||||
/// </summary>
|
||||
public static string Key(string? url) => (url ?? "").Trim().TrimEnd('/');
|
||||
|
||||
/// <summary>
|
||||
/// Returns the absolute http(s) base address with exactly one trailing slash, so
|
||||
/// relative combines replace no path segment. Throws for any other address.
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
using System.Security.Cryptography;
|
||||
using OpenNest.Data;
|
||||
using OpenNest.Shapes;
|
||||
|
||||
namespace OpenNest.Tests.Data;
|
||||
|
||||
public class LibraryDrawingInsertTests
|
||||
{
|
||||
private const string Server = "http://library.test:8090";
|
||||
private static readonly Guid LibraryId = Guid.Parse("3f1e2d4c-5b6a-4987-8a1b-2c3d4e5f6a7b");
|
||||
private static readonly byte[] PackageBytes = { (byte)'P', (byte)'K', 3, 4, 1, 2 };
|
||||
|
||||
private static Nest MatchingNest() => new("Synthetic job")
|
||||
{
|
||||
Units = Units.Inches,
|
||||
Thickness = 0.25,
|
||||
Material = new Material("Synthetic Steel", "Grade A"),
|
||||
};
|
||||
|
||||
private static DrawingLibraryRecord Record(int revision = 3, string name = "SYN-101", byte[]? package = null) => new()
|
||||
{
|
||||
Id = LibraryId,
|
||||
Revision = revision,
|
||||
Name = name,
|
||||
Customer = "Synthetic customer",
|
||||
MaterialName = "Synthetic Steel",
|
||||
MaterialGrade = "Grade A",
|
||||
Thickness = 0.25,
|
||||
Units = Units.Inches,
|
||||
CutType = CutType.Plasma,
|
||||
Notes = "Deburr",
|
||||
PackageSha256 = Convert.ToHexString(SHA256.HashData(package ?? PackageBytes)).ToLowerInvariant(),
|
||||
};
|
||||
|
||||
private static LibraryPackage Package(Units units = Units.Inches, IReadOnlyList<string>? warnings = null)
|
||||
{
|
||||
var drawing = new RectangleShape { Name = "old package name", Length = 4, Width = 2 }.GetDrawing();
|
||||
drawing.Material = new Material("Package material", "Package grade", 0.284);
|
||||
drawing.Customer = "Package customer";
|
||||
return new LibraryPackage(PackageBytes, drawing, units, warnings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Matching_AddsAnExactCopyCarryingTheLinkAndKeepsPins()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
var package = Package();
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server + "/", Record(), package, 5);
|
||||
|
||||
Assert.Equal(LibraryInsertKind.AddDrawing, insert.Kind);
|
||||
Assert.True(insert.KeepPins);
|
||||
Assert.Empty(insert.Warnings);
|
||||
Assert.Empty(nest.Drawings);
|
||||
|
||||
insert.Apply();
|
||||
|
||||
var drawing = Assert.Single(nest.Drawings);
|
||||
Assert.Same(package.Drawing, drawing);
|
||||
Assert.Equal(("SYN-101", "Synthetic customer", 5), (drawing.Name, drawing.Customer, drawing.Quantity.Required));
|
||||
Assert.Equal(("Synthetic Steel", "Grade A", 0.284), (drawing.Material.Name, drawing.Material.Grade, drawing.Material.Density));
|
||||
var link = drawing.Library!;
|
||||
Assert.Equal(
|
||||
(Server, LibraryId, 3, CutType.Plasma, 0.25, "Synthetic Steel", "Grade A", Units.Inches, "Deburr", Record().PackageSha256),
|
||||
(link.ServerKey, link.Id, link.Revision, link.CutType, link.Thickness, link.MaterialName, link.MaterialGrade,
|
||||
link.Units, link.Notes, link.PackageSha256));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpacingCaseAndThicknessRounding_StillMatch()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
nest.Material = new Material(" synthetic STEEL ", "grade a");
|
||||
nest.Thickness = 0.250004;
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1);
|
||||
|
||||
Assert.True(insert.KeepPins);
|
||||
Assert.Empty(insert.Warnings);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Other Steel", "Grade A", 0.25, "material Synthetic Steel (nest Other Steel)")]
|
||||
[InlineData("Synthetic Steel", "Grade B", 0.25, "grade Grade A (nest Grade B)")]
|
||||
[InlineData("Synthetic Steel", "Grade A", 0.1875, "thickness 0.25 (nest 0.1875)")]
|
||||
[InlineData("", "", 0.0, "material Synthetic Steel (nest not set)")]
|
||||
public void Mismatch_AddsButDropsPinsAndNamesTheDifference(string material, string grade, double thickness, string expected)
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
nest.Material = new Material(material, grade);
|
||||
nest.Thickness = thickness;
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 2);
|
||||
|
||||
Assert.Equal(LibraryInsertKind.AddDrawing, insert.Kind);
|
||||
Assert.False(insert.KeepPins);
|
||||
var warning = Assert.Single(insert.Warnings);
|
||||
Assert.Contains(expected, warning);
|
||||
Assert.Contains("'SYN-101'", warning);
|
||||
Assert.Contains("cut pins are dropped", warning);
|
||||
insert.Apply();
|
||||
Assert.Single(nest.Drawings);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryDifference_IsNamed()
|
||||
{
|
||||
var nest = new Nest("Other job") { Units = Units.Inches, Thickness = 0.1875, Material = new Material("Other Steel", "Grade B") };
|
||||
|
||||
var warning = Assert.Single(LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1).Warnings);
|
||||
|
||||
Assert.Contains("material Synthetic Steel (nest Other Steel)", warning);
|
||||
Assert.Contains("grade Grade A (nest Grade B)", warning);
|
||||
Assert.Contains("thickness 0.25 (nest 0.1875)", warning);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageWarnings_AreShownBeforeTheAdd()
|
||||
{
|
||||
var insert = LibraryDrawingInsert.Plan(MatchingNest(), Server, Record(),
|
||||
Package(warnings: new[] { "reader note" }), 1);
|
||||
|
||||
Assert.Equal(new[] { "reader note" }, insert.Warnings);
|
||||
Assert.True(insert.KeepPins);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentUnits_AreRefused()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
nest.Units = Units.Millimeters;
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1);
|
||||
|
||||
AssertRefused(insert, nest, "inches", "millimeters");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PackageDisagreeingWithItsRecord_IsRefused()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
|
||||
AssertRefused(LibraryDrawingInsert.Plan(nest, Server, Record(), Package(Units.Millimeters), 1), nest, "library record says inches");
|
||||
AssertRefused(LibraryDrawingInsert.Plan(nest, Server, Record(package: new byte[] { 9 }), Package(), 1), nest, "does not match");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameRevisionAlreadyInTheNest_AddsQuantityOnly()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 4).Apply();
|
||||
var existing = Assert.Single(nest.Drawings);
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 3);
|
||||
|
||||
Assert.Equal(LibraryInsertKind.AddQuantity, insert.Kind);
|
||||
Assert.Same(existing, insert.Drawing);
|
||||
insert.Apply();
|
||||
Assert.Same(existing, Assert.Single(nest.Drawings));
|
||||
Assert.Equal(7, existing.Quantity.Required);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameRevisionRenamedInTheNest_StillAddsQuantity()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
var first = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1);
|
||||
first.Drawing!.Name = "renamed";
|
||||
first.Apply();
|
||||
|
||||
var insert = LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1);
|
||||
|
||||
Assert.Equal(LibraryInsertKind.AddQuantity, insert.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OtherRevisionOfTheSameDrawing_IsRefused()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
var first = LibraryDrawingInsert.Plan(nest, Server, Record(revision: 2), Package(), 1);
|
||||
first.Drawing!.Name = "renamed";
|
||||
first.Apply();
|
||||
|
||||
AssertRefused(LibraryDrawingInsert.Plan(nest, Server, Record(revision: 3), Package(), 1), nest,
|
||||
"revision 2", "'renamed'", "Update Drawings");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DifferentDrawingWithTheSameName_IsRefusedNamingBoth()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
nest.Drawings.Add(new RectangleShape { Name = "SYN-101", Length = 1, Width = 1 }.GetDrawing());
|
||||
|
||||
AssertRefused(LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1), nest,
|
||||
"'SYN-101' (not from the drawing library)", "library drawing 'SYN-101' (Synthetic customer) revision 3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameRevisionFromAnotherServer_IsADifferentDrawing()
|
||||
{
|
||||
var nest = MatchingNest();
|
||||
LibraryDrawingInsert.Plan(nest, "http://other.test:8090", Record(), Package(), 1).Apply();
|
||||
|
||||
AssertRefused(LibraryDrawingInsert.Plan(nest, Server, Record(), Package(), 1), nest,
|
||||
"library revision 3 from http://other.test:8090");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Apply_RunsOnce()
|
||||
{
|
||||
var insert = LibraryDrawingInsert.Plan(MatchingNest(), Server, Record(), Package(), 1);
|
||||
insert.Apply();
|
||||
|
||||
Assert.Throws<InvalidOperationException>(insert.Apply);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_RefusesMissingInputs()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => LibraryDrawingInsert.Plan(MatchingNest(), Server, Record(), Package(), 0));
|
||||
Assert.Throws<ArgumentException>(() => LibraryDrawingInsert.Plan(MatchingNest(), " / ", Record(), Package(), 1));
|
||||
}
|
||||
|
||||
private static void AssertRefused(LibraryDrawingInsert insert, Nest nest, params string[] fragments)
|
||||
{
|
||||
var before = nest.Drawings.Select(d => (d, d.Quantity.Required)).ToList();
|
||||
|
||||
Assert.True(insert.IsRefused);
|
||||
Assert.Null(insert.Drawing);
|
||||
foreach (var fragment in fragments)
|
||||
Assert.Contains(fragment, insert.Refusal);
|
||||
Assert.Throws<InvalidOperationException>(insert.Apply);
|
||||
Assert.Equal(before, nest.Drawings.Select(d => (d, d.Quantity.Required)).ToList());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user