Files
OpenNest/OpenNest.Tests/Data/MaterialLedgerSettingsTests.cs
T
ajandClaude Opus 5.5 ea1decc097 feat(autonest): load sheet and drop stock from MaterialLedger
Auto Nest's Stock Options gets a Load from ledger button that reads the nest
material's free sheets and drops from MaterialLedger's API-key-protected
GET /api/stock/opennest. Ledger rows are request-local: the engine sees them as
ordinary NestPlateStock rows with ledger:{n} ids and finite quantities, and they
are never written to Nest.PlateOptions.

- OpenNest.Data/MaterialLedger: settings file, read-only client (X-Api-Key,
  5 s timeout, closed LedgerStockResult set) and LedgerStockMapper (bundles and
  sheets one row each, same-size drops grouped with ordered item codes, unknown
  shapes and too-small items skipped, inch/mm conversion).
- Options: MaterialLedger URL and API key (DPAPI, CurrentUser) with Test.
- Auto Nest grid: Use, Qty and Source columns for every row; PlateOption gains
  request-local Quantity and Id, which NestStockBuilder passes to the engine.
  Ledger rows score by area; hand-typed costs stay saved.

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

66 lines
2.4 KiB
C#

using OpenNest.Data.MaterialLedger;
namespace OpenNest.Tests.Data;
public class MaterialLedgerSettingsTests : IDisposable
{
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-ledger-" + Guid.NewGuid().ToString("N"));
private string SettingsPath => Path.Combine(directory, "material-ledger.json");
private static string Protect(string plain) => "P:" + new string(plain.Reverse().ToArray());
private static string Unprotect(string text) =>
text.StartsWith("P:") ? new string(text[2..].Reverse().ToArray()) : throw new FormatException("not ours");
[Fact]
public void RoundTripStoresOnlyTheProtectedKey()
{
new MaterialLedgerSettings { BaseUrl = " http://ledger:5000/ ", ApiKey = "secret-key" }.Save(SettingsPath, Protect);
var json = File.ReadAllText(SettingsPath);
Assert.DoesNotContain("secret-key", json);
Assert.Contains("yek-terces", json);
var loaded = MaterialLedgerSettings.Load(SettingsPath, Unprotect);
Assert.Equal("http://ledger:5000", loaded.BaseUrl);
Assert.Equal("secret-key", loaded.ApiKey);
Assert.True(loaded.IsConfigured);
}
[Fact]
public void KeyThatCannotBeUnprotectedLoadsEmpty()
{
new MaterialLedgerSettings { BaseUrl = "http://ledger", ApiKey = "secret-key" }.Save(SettingsPath, _ => "foreign blob");
var loaded = MaterialLedgerSettings.Load(SettingsPath, Unprotect);
Assert.Equal("http://ledger", loaded.BaseUrl);
Assert.Equal("", loaded.ApiKey);
}
[Fact]
public void MissingOrCorruptFileIsNotConfigured()
{
Assert.False(MaterialLedgerSettings.Load(SettingsPath, Unprotect).IsConfigured);
Directory.CreateDirectory(directory);
File.WriteAllText(SettingsPath, "{ not json");
Assert.False(MaterialLedgerSettings.Load(SettingsPath, Unprotect).IsConfigured);
}
[Theory]
[InlineData("", true, "")]
[InlineData(" https://ledger/ ", true, "https://ledger")]
[InlineData("ledger", false, "")]
[InlineData("ftp://ledger", false, "")]
public void UrlsAreValidatedAndNormalized(string input, bool valid, string expected)
{
Assert.Equal(valid, MaterialLedgerSettings.TryNormalizeUrl(input, out var normalized));
Assert.Equal(expected, normalized);
}
public void Dispose()
{
if (Directory.Exists(directory))
Directory.Delete(directory, recursive: true);
}
}