using System; using System.IO; using System.Security.Claims; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Xunit; using Yavsc.Models; using Yavsc.Models.Billing; using Yavsc.Server.Helpers; using Yavsc.Server.Models.FileSystem; namespace Yavsc.Org.Tests; /// /// Tests for the static /// helper. Scope is intentionally narrow: the file-naming format, /// the strokes counter, and the on-disk write path. The controller /// (authz, db persistence, signalR notification) is out of scope /// for this commit and will get a dedicated integration test once /// the Yavsc.Api test project is set up. /// public class EstimateSignatureFileHelperTests : IDisposable { private readonly string _tempRoot; public EstimateSignatureFileHelperTests() { // UserFilesDirName is a process-wide static; we redirect // it to a per-test temp dir so concurrent tests don't // collide and the host filesystem is not littered. _tempRoot = Path.Combine( Path.GetTempPath(), "yavsc-sig-tests-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempRoot); AbstractFileSystemHelpers.UserFilesDirName = _tempRoot; } public void Dispose() { try { Directory.Delete(_tempRoot, recursive: true); } catch { /* best effort — the OS will clean Temp eventually */ } } [Fact] public void FileNameFormat_lowercases_type_and_includes_estimateId_and_ticks() { var name = EstimateSignatureFileHelper.FileNameFormat( SignatureType.Pro, 42, 638_000_000_000_000_000L); Assert.Equal("sign-pro-42-638000000000000000.json", name); var cli = EstimateSignatureFileHelper.FileNameFormat( SignatureType.Client, 7, 1L); Assert.Equal("sign-client-7-1.json", cli); } [Theory] [InlineData(new int[] { }, 0)] [InlineData(new[] { 1, 100, 200 }, 1)] [InlineData(new[] { 2, 1, 2, 3, 4 }, 1)] [InlineData(new[] { 1, 1, 1, 2, 2, 3, 3 }, 2)] [InlineData(new[] { 0, 1, 2, 3 }, 0)] // malformed k=0: short-circuit public void ReceiveEstimateSignatureAsync_writes_a_v1_envelope(int[] strokes, int expectedStrokeCount) { // We don't read the count back from the helper (it's a // private method), but the JSON envelope must reflect // it; this verifies the public behaviour end-to-end. _ = expectedStrokeCount; // Arrange var user = MakeUser("alice"); var payload = new SignaturePadPayload { CoordinateMax = 10_000, CapturedAtUtc = new DateTime(2026, 7, 4, 12, 0, 0, DateTimeKind.Utc), Strokes = strokes, }; // Act var fi = Run(user, 123L, SignatureType.Pro, payload); // Assert: file exists, sits under the user's root, and // parses as a yavsc.signature/v1 envelope. var fullPath = Path.Combine(fi.DestDir, fi.FileName); Assert.True(File.Exists(fullPath), $"missing: {fullPath}"); using var doc = JsonDocument.Parse(File.ReadAllText(fullPath)); var root = doc.RootElement; Assert.Equal("yavsc.signature/v1", root.GetProperty("format").GetString()); Assert.Equal(10_000, root.GetProperty("coordinateMax").GetInt32()); Assert.Equal(123L, root.GetProperty("estimateId").GetInt64()); Assert.Equal("Pro", root.GetProperty("type").GetString()); Assert.Equal("alice", root.GetProperty("signerName").GetString()); Assert.Equal(expectedStrokeCount, root.GetProperty("strokeCount").GetInt32()); } [Fact] public async Task ReceiveEstimateSignatureAsync_rejects_null_payload() { var user = MakeUser("bob"); await Assert.ThrowsAsync(() => EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( user, 1L, SignatureType.Pro, payload: null!)); } [Fact] public async Task ReceiveEstimateSignatureAsync_rejects_non_positive_estimateId() { var user = MakeUser("bob"); var payload = new SignaturePadPayload { Strokes = new[] { 1, 100, 100 } }; await Assert.ThrowsAsync(() => EstimateSignatureFileHelper.ReceiveEstimateSignatureAsync( user, 0L, SignatureType.Pro, payload)); } // --- helpers ---------------------------------------------------- private static FileReceivedInfo Run( ClaimsPrincipal user, long estimateId, SignatureType type, SignaturePadPayload payload) { // The helper is async; tests that don't care about the // result can call it sync via .GetAwaiter().GetResult() // because we know it never throws in the happy path. return EstimateSignatureFileHelper .ReceiveEstimateSignatureAsync(user, estimateId, type, payload, CancellationToken.None) .GetAwaiter().GetResult(); } private static ClaimsPrincipal MakeUser(string username) { return new ClaimsPrincipal(new ClaimsIdentity( new[] { new Claim(ClaimTypes.Name, username) }, authenticationType: "test")); } }