using System.Security.Cryptography; using OpenNest.Data; namespace OpenNest.Tests.Data; /// /// An in-memory drawing library server: revisions, If-Match conflicts, archive, the /// query filters the server applies, a write log and a switch that fails the next write. /// Linked into OpenNest.WinForms.Tests as well. /// internal sealed class InMemoryDrawingLibrary : IDrawingLibraryStore { private readonly Dictionary> _revisions = new(); private readonly Dictionary _packages = new(); private DateTime _clock = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); /// Fail the write with this index (0 is the next write) without storing it. public int? FailWrite { get; set; } /// Every write requested, in order: ("upload" | "update", id). public List<(string Kind, Guid Id)> Writes { get; } = new(); public IEnumerable Current => _revisions.Values.Select(r => r[^1]); public DrawingLibraryRecord Get(Guid id) => _revisions[id][^1]; /// Stores revision 1 directly, as though another operator had saved it. public DrawingLibraryRecord Seed(string name, string material, double thickness, byte[] package, string customer = "", Units units = Units.Inches, CutType cutType = CutType.Laser, bool archived = false) { var id = Guid.NewGuid(); var record = Stored(new DrawingLibraryRecord { Name = name, Customer = customer, MaterialName = material, Thickness = thickness, Units = units, CutType = cutType, Notes = "Seeded notes", }, id, 1, package); record.Archived = archived; _revisions[id] = new List { record }; return record; } public Task QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default) { var search = query.NormalizedSearch; var matches = Current .Where(r => query.IncludeArchived || !r.Archived) .Where(r => search.Length == 0 || new[] { r.Name, r.Customer, r.Notes } .Any(text => text.Contains(search, StringComparison.OrdinalIgnoreCase))) .Where(r => Filter(query.Customer, r.Customer) && Filter(query.MaterialName, r.MaterialName) && Filter(query.MaterialGrade, r.MaterialGrade)) .Where(r => query.Thickness is not { } t || r.Thickness == DrawingLibraryRules.NormalizeThickness(t)) .Where(r => query.CutType is not { } c || r.CutType == c) .OrderByDescending(r => r.SavedAt) .ToList(); return Task.FromResult(new DrawingLibraryPage { Items = matches.Skip(query.Offset).Take(query.Limit).ToList(), Total = matches.Count, Offset = query.Offset, Limit = query.Limit, }); } /// Every package download requested: (id, revision). public List<(Guid Id, int Revision)> Downloads { get; } = new(); public Task GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) { Downloads.Add((id, revision)); var record = _revisions.TryGetValue(id, out var list) ? list.FirstOrDefault(r => r.Revision == revision) : null; return Task.FromResult(record != null && _packages.TryGetValue(record.PackageSha256, out var package) ? package.ToArray() : null); } public Task GetAsync(Guid id, CancellationToken cancellationToken = default) => Task.FromResult(_revisions.TryGetValue(id, out var list) ? list[^1] : null); public Task GetRevisionsAsync(Guid id, int offset = 0, int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) { if (!_revisions.TryGetValue(id, out var list)) return Task.FromResult(null); var newestFirst = Enumerable.Reverse(list).ToList(); return Task.FromResult(new DrawingLibraryPage { Items = newestFirst.Skip(offset).Take(limit).ToList(), Total = list.Count, Offset = offset, Limit = limit, }); } public Task UploadAsync(DrawingLibraryRecord record, byte[] package, CancellationToken cancellationToken = default) { Write("upload", record.Id); if (_revisions.TryGetValue(record.Id, out var existing)) throw new DrawingLibraryConflictException(existing[^1]); _revisions[record.Id] = new List { Stored(record, record.Id, 1, package) }; return Task.FromResult(Get(record.Id)); } public Task UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package, int expectedRevision, CancellationToken cancellationToken = default) { Write("update", id); var current = Get(id); if (current.Revision != expectedRevision || current.Archived) throw new DrawingLibraryConflictException(current); var next = Stored(record, id, current.Revision + 1, package); _revisions[id].Add(next); return Task.FromResult(next); } public Task UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision, CancellationToken cancellationToken = default) => throw new NotSupportedException(); public static string Sha(byte[] bytes) => Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant(); private void Write(string kind, Guid id) { var index = Writes.Count; Writes.Add((kind, id)); if (FailWrite == index) throw new HttpRequestException("Synthetic connection failure."); } private DrawingLibraryRecord Stored(DrawingLibraryRecord metadata, Guid id, int revision, byte[] package) { _clock = _clock.AddMinutes(1); _packages[Sha(package)] = package.ToArray(); return new DrawingLibraryRecord { Id = id, Revision = revision, Name = metadata.Name, Customer = metadata.Customer, MaterialName = metadata.MaterialName, MaterialGrade = metadata.MaterialGrade, Thickness = DrawingLibraryRules.NormalizeThickness(metadata.Thickness), Units = metadata.Units, CutType = metadata.CutType, Notes = metadata.Notes, Settings = metadata.Settings, SavedBy = metadata.SavedBy, PackageSha256 = Sha(package), PackageSize = package.Length, SavedAt = _clock, }; } private static bool Filter(string filter, string value) => string.IsNullOrWhiteSpace(filter) || SharedListNames.Key(filter) == SharedListNames.Key(value); }