using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json.Linq; namespace Creditcall.ChipDna.Client { internal static class StreamingTests { private static int Main() { var tests = new Action[] { SharedExecutionReturnsAuthoritativeResultOnce, TimeoutDoesNotCancelCompletion, StartAndTaskErrorsKeepExceptionBehavior, SubscriptionIsRemovedOnSuccessAndException, OwnershipAcceptsReferenceLessProgressAndSuppressesPermanently, RepeatedReferenceSuppressesProgress, NativeMiuraSequenceKeepsFinalAuthoritative, NativeNotificationUsesNotificationKey, NativeOwnershipAndRemovalBoundaries, PreDispatchFailureReleasesOnlyUnusedReference, UnprovenStartFailuresSuppressObservation, AsyncErrorSuppressesOnlyActiveObservation, TimeoutUnsubscribesAndKeepsFuturePaymentsEnabled, QueueCapacityCannotDropFinal, ConcurrentCallbacksKeepValidLinesAndOneFinal, DisconnectDoesNotInterruptExecution, FinalFieldsExcludeSensitiveParameters }; int failed = 0; foreach (var test in tests) { try { test(); Console.WriteLine("PASS " + test.Method.Name); } catch (Exception ex) { failed++; Console.WriteLine("FAIL " + test.Method.Name + ": " + ex); } } Console.WriteLine($"{tests.Length - failed}/{tests.Length} tests passed"); return failed == 0 ? 0 : 1; } private static void Check(bool condition, string message) { if (!condition) throw new Exception(message); } private static EventParameters Native(string key, string value, string reference = null) { var fields = new Dictionary(); if (key != null) fields[key] = value; if (reference != null) fields["REFERENCE"] = reference; // SDK event construction is internal; no ClientHelper or terminal is started. return (EventParameters)Activator.CreateInstance(typeof(EventParameters), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { fields }, null); } private static void CheckUnsubscribed(TransactionProgressTap tap) { var listener = typeof(TransactionProgressTap).GetField("listener", BindingFlags.Instance | BindingFlags.NonPublic); Check(listener.GetValue(tap) == null, "progress subscription survived execution"); } private static void SharedExecutionReturnsAuthoritativeResultOnce() { foreach (var outcome in new[] { "Approved", "Declined", "Cancelled" }) { var expected = new Dictionary { { "TRANSACTION_RESULT", outcome }, { "REFERENCE", "existing-ref" } }; var source = new TaskCompletionSource>(); int starts = 0, suppressions = 0; var result = TransactionExecution.Run(() => { starts++; source.SetResult(expected); }, () => source.Task, () => suppressions++, TimeSpan.FromSeconds(1)); Check(ReferenceEquals(expected, result) && starts == 1 && suppressions == 0, "shared execution changed result or start count for " + outcome); } } private static void TimeoutDoesNotCancelCompletion() { var source = new TaskCompletionSource>(); int starts = 0, suppressions = 0; var result = TransactionExecution.Run(() => starts++, () => source.Task, () => suppressions++, TimeSpan.Zero); Check(result["TRANSACTION_RESULT"] == "TIMEOUT" && result["ERRORS"] == "Transaction timed out waiting for device response", "timeout fields changed"); Check(starts == 1 && suppressions == 1 && !source.Task.IsCompleted, "timeout cancelled or restarted the transaction"); source.SetResult(new Dictionary { { "TRANSACTION_RESULT", "Approved" } }); Check(source.Task.Result["TRANSACTION_RESULT"] == "Approved", "late SDK completion was disabled"); } private static void StartAndTaskErrorsKeepExceptionBehavior() { int suppressions = 0; try { TransactionExecution.Run(() => { throw new InvalidOperationException("start"); }, () => Task.FromResult(new Dictionary()), () => suppressions++, TimeSpan.Zero); throw new Exception("expected original start exception"); } catch (InvalidOperationException) { } var fault = new TaskCompletionSource>(); fault.SetException(new IOException("SDK fault")); try { TransactionExecution.Run(() => { }, () => fault.Task, () => suppressions++, TimeSpan.FromSeconds(1)); throw new Exception("expected original Task.Wait AggregateException"); } catch (AggregateException) { } Check(suppressions == 2, "ambiguous exception did not suppress observation"); } private static void SubscriptionIsRemovedOnSuccessAndException() { var tap = new TransactionProgressTap(); int first = 0, second = 0; tap.Observe((s,v) => first++, () => { tap.Begin("first"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "first")); tap.End(); return new Dictionary(); }); CheckUnsubscribed(tap); tap.Begin("after-first"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "after-first")); tap.End(); try { tap.Observe((s,v) => second++, () => { tap.Begin("second"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "second")); throw new IOException("test"); }); } catch (IOException) { } tap.Report("UPDATE", Native("UPDATE", "CardRequested", "second")); Check(first == 1 && second == 1, $"subscriptions leaked: {first}/{second}"); CheckUnsubscribed(tap); } private static void OwnershipAcceptsReferenceLessProgressAndSuppressesPermanently() { var tap = new TransactionProgressTap(); int callbacks = 0, payments = 0; tap.Observe((s,v) => callbacks++, () => { payments++; tap.Begin("first"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", null)); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "other")); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "first")); tap.Suppress(); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "first")); tap.End(); return new Dictionary(); }); tap.Observe((s,v) => callbacks++, () => { payments++; tap.Begin("later"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "first")); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "later")); tap.End(); return new Dictionary(); }); Check(callbacks == 2 && payments == 2, "active reference-less progress was dropped, suppression leaked, or later payments were disabled"); } private static void RepeatedReferenceSuppressesProgress() { var tap = new TransactionProgressTap(); int callbacks = 0; tap.Observe((s,v) => callbacks++, () => { tap.Begin("same-second-reference"); tap.End(); tap.Begin("same-second-reference"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "same-second-reference")); return new Dictionary(); }); Check(callbacks == 0, "reused reference attributed an ambiguous event"); } private static void NativeMiuraSequenceKeepsFinalAuthoritative() { foreach (var outcome in new[] { "Approved", "Declined" }) using (var output = new MemoryStream()) { var tap = new TransactionProgressTap(); var writer = new TransactionStreamWriter(output); var expected = new Dictionary { { "TRANSACTION_RESULT", outcome }, { "REFERENCE", "miura-ref" } }; int starts = 0; var result = tap.Observe(writer.Progress, () => TransactionExecution.Run(() => { starts++; tap.Begin("miura-ref"); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Report("CARD_STATUS", Native("NOTIFICATION", "Inserted")); foreach (var update in new[] { "TransactionStarted", "PinEntryStarted", "OnlineAuthRequested", "OnlineAuthCompleted" }) tap.Report("UPDATE", Native("UPDATE", update)); tap.End(); // TransactionFinished closes observation before final processing. tap.Report("UPDATE", Native("UPDATE", "CardRemovalRequested")); tap.Report("CARD_STATUS", Native("NOTIFICATION", "Removed")); }, () => Task.FromResult(expected), tap.Suppress, TimeSpan.FromSeconds(1))); writer.Complete(new { type = "result", result }); writer.Deliver(); var frames = Frames(output); var values = frames.Take(frames.Length - 1).Select(f => f["value"].Value()); Check(values.SequenceEqual(new[] { "CardRequested", "CardRequested", "Inserted", "TransactionStarted", "PinEntryStarted", "OnlineAuthRequested", "OnlineAuthCompleted" }), "SDK 3.17 Miura sequence lost or changed normal reference-less progress"); Check(starts == 1 && ReferenceEquals(result, expected) && frames.Length == 8 && frames.Last()["result"]["TRANSACTION_RESULT"].Value() == outcome, "progress changed start count or authoritative " + outcome + " result"); CheckUnsubscribed(tap); } } private static void NativeNotificationUsesNotificationKey() { var tap = new TransactionProgressTap(); var delivered = new List(); tap.Observe((source, value) => delivered.Add(source + ":" + value), () => { tap.Begin("native-key"); tap.Report("CARD_STATUS", Native("CARD_STATUS", "Inserted")); foreach (var value in new[] { "Inserted", "Tapped", "Swiped" }) tap.Report("CARD_STATUS", Native("NOTIFICATION", value)); tap.Report("CARD_STATUS", Native("NOTIFICATION", "Removed", "native-key")); tap.Report("CARD_STATUS", Native("NOTIFICATION", "Unknown")); tap.Report("UPDATE", Native("PAN", "4761730000001133")); tap.Report("UPDATE", null); tap.End(); return new Dictionary(); }); Check(delivered.SequenceEqual(new[] { "CARD_STATUS:Inserted", "CARD_STATUS:Tapped", "CARD_STATUS:Swiped" }), "native NOTIFICATION extraction used CARD_STATUS, exposed card data, or changed the wire source"); } private static void NativeOwnershipAndRemovalBoundaries() { var tap = new TransactionProgressTap(); var delivered = new List(); tap.Observe((source, value) => delivered.Add(source + ":" + value), () => { tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Begin("first"); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "")); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted", "other")); tap.Report("SIGNATURE", Native(null, null)); tap.Report("PAUSE", Native("PAUSE_STATE", "PostCardDetails")); foreach (var removal in new[] { "CardRemovalRequested", "CardRemovalEnforced" }) { tap.Report("UPDATE", Native("UPDATE", removal)); tap.Report("UPDATE", Native("UPDATE", removal, "other")); tap.Report("UPDATE", Native("UPDATE", removal, "first")); } tap.End(); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted", "first")); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Begin("next"); tap.Report("UPDATE", Native("UPDATE", "CardRemovalRequested")); tap.Report("UPDATE", Native("UPDATE", "CardRemovalEnforced")); tap.Report("CARD_STATUS", Native("NOTIFICATION", "Removed")); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted", "first")); // Accepted best-effort limitation: unidentified non-removal progress // is allowed in this new window even if it came from an older queue. tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Report("UPDATE", Native("UPDATE", "Approved")); tap.End(); return new Dictionary(); }); Check(delivered.SequenceEqual(new[] { "UPDATE:CardRequested", "SIGNATURE:Requested", "PAUSE:Paused", "UPDATE:CardRemovalRequested", "UPDATE:CardRemovalEnforced", "UPDATE:CardRequested" }), "active-window, explicit-reference, or removal ownership changed"); } private static void PreDispatchFailureReleasesOnlyUnusedReference() { var tap = new TransactionProgressTap(); int starts = 0, callbacks = 0; foreach (var rejected in new[] { true, false }) { var expected = rejected ? new Dictionary { { "ERRORS", "ClientNotConnectedToServer" }, { "REFERENCE", "same-second" } } : new Dictionary { { "TRANSACTION_RESULT", "Approved" }, { "REFERENCE", "same-second" } }; var result = tap.Observe((source, value) => callbacks++, () => TransactionExecution.Run(() => { starts++; tap.Begin("same-second"); if (rejected) tap.StartFailed("ClientNotConnectedToServer"); else tap.Report("UPDATE", Native("UPDATE", "CardRequested")); if (!rejected) tap.End(); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted")); }, () => Task.FromResult(expected), tap.Suppress, TimeSpan.FromSeconds(1))); Check(ReferenceEquals(result, expected), "pre-dispatch handling changed the financial result"); CheckUnsubscribed(tap); } Check(starts == 2 && callbacks == 1, "proven pre-dispatch rejection prevented a safe same-reference retry"); } private static void UnprovenStartFailuresSuppressObservation() { foreach (var error in new[] { "AmountTooSmall", "MissingParameter", "NoPinPadsAvailable", "TransactionInProgress", "ClientNotConnectedToServer,AmountTooSmall", " ClientNotConnectedToServer", "500005", "Unknown" }) { var tap = new TransactionProgressTap(); int starts = 0, callbacks = 0; foreach (var first in new[] { true, false }) { var expected = first ? new Dictionary { { "ERRORS", error } } : new Dictionary { { "TRANSACTION_RESULT", "Approved" } }; var result = tap.Observe((source, value) => callbacks++, () => TransactionExecution.Run(() => { starts++; tap.Begin(first ? "failed" : "later"); if (first) tap.StartFailed(error); // A subsequent safe rejection must never clear an existing latch. tap.StartFailed("ClientNotConnectedToServer"); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Report("UPDATE", Native("UPDATE", "CardRequested", "later")); tap.End(); }, () => Task.FromResult(expected), tap.Suppress, TimeSpan.FromSeconds(1))); Check(ReferenceEquals(result, expected), "observational handling changed result for " + error); } Check(starts == 2 && callbacks == 0, "unproven error recovered progress or disabled payment: " + error); } } private static void AsyncErrorSuppressesOnlyActiveObservation() { var tap = new TransactionProgressTap(); int callbacks = 0, starts = 0; tap.SuppressIfActive(); // Idle SDK errors have no active transaction to make ambiguous. foreach (var reference in new[] { "first", "later" }) { var expected = new Dictionary { { "TRANSACTION_RESULT", "Approved" } }; var result = tap.Observe((source, value) => callbacks++, () => TransactionExecution.Run(() => { starts++; tap.Begin(reference); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.SuppressIfActive(); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted")); tap.End(); }, () => Task.FromResult(expected), tap.Suppress, TimeSpan.FromSeconds(1))); Check(ReferenceEquals(result, expected), "asynchronous SDK error altered financial execution"); } Check(starts == 2 && callbacks == 1, "asynchronous error failed to latch suppression or suppressed while idle"); } private static void TimeoutUnsubscribesAndKeepsFuturePaymentsEnabled() { var tap = new TransactionProgressTap(); var completion = new TaskCompletionSource>(); int starts = 0, callbacks = 0; var timeout = tap.Observe((source, value) => callbacks++, () => TransactionExecution.Run(() => { starts++; tap.Begin("timeout"); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); }, () => completion.Task, tap.Suppress, TimeSpan.Zero)); Check(timeout["TRANSACTION_RESULT"] == "TIMEOUT" && !completion.Task.IsCompleted, "timeout changed financial completion"); CheckUnsubscribed(tap); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted")); completion.SetResult(new Dictionary { { "TRANSACTION_RESULT", "Approved" } }); tap.End(); var expected = new Dictionary { { "TRANSACTION_RESULT", "Declined" } }; var later = tap.Observe((source, value) => callbacks++, () => TransactionExecution.Run(() => { starts++; tap.Begin("later"); tap.Report("UPDATE", Native("UPDATE", "CardRequested")); tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted", "later")); tap.End(); }, () => Task.FromResult(expected), tap.Suppress, TimeSpan.FromSeconds(1))); Check(starts == 2 && callbacks == 1 && ReferenceEquals(later, expected), "timeout suppression leaked progress or blocked future payments"); CheckUnsubscribed(tap); } private static JObject[] Frames(MemoryStream stream) { var text = Encoding.UTF8.GetString(stream.ToArray()); Check(text.Length == 0 || text[0] != '\uFEFF', "stream contained a BOM"); return text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(JObject.Parse).ToArray(); } private static void QueueCapacityCannotDropFinal() { using (var output = new MemoryStream()) { var writer = new TransactionStreamWriter(output); for (int i = 0; i < 1000; i++) writer.Progress("UPDATE", "CardRequested"); writer.Complete(new { type = "result", result = new { TRANSACTION_RESULT = "Approved" } }); writer.Complete(new { type = "error", error = "duplicate" }); writer.Progress("UPDATE", "PinEntryStarted"); writer.Deliver(); writer.Deliver(); var frames = Frames(output); Check(frames.Length == 129 && frames.Last()["type"].Value() == "result", "full queue dropped or duplicated final"); Check(frames.Take(128).All(f => f["value"].Value() == "CardRequested"), "progress arrived after final"); } } private static void ConcurrentCallbacksKeepValidLinesAndOneFinal() { using (var output = new MemoryStream()) { var writer = new TransactionStreamWriter(output); var delivery = Task.Run(() => writer.Deliver()); var tap = new TransactionProgressTap(); tap.Observe(writer.Progress, () => { tap.Begin("concurrent"); Parallel.For(0, 5000, i => tap.Report("UPDATE", Native("UPDATE", "PinEntryStarted"))); tap.End(); return new Dictionary(); }); writer.Complete(new { type = "result", result = new { TRANSACTION_RESULT = "Declined" } }); Check(delivery.Wait(TimeSpan.FromSeconds(5)), "writer did not finish"); var frames = Frames(output); Check(frames.Count(f => f["type"].Value() == "result") == 1 && frames.Last()["type"].Value() == "result", "concurrent callbacks corrupted terminal framing"); Check(frames.Where(f => f["type"].Value() == "status").All(f => f["source"].Value() == "UPDATE" && f["value"].Value() == "PinEntryStarted"), "concurrent callbacks corrupted status fields"); } } private sealed class DisconnectedStream : MemoryStream { public override void Write(byte[] buffer, int offset, int count) { throw new IOException("disconnected"); } } private static void DisconnectDoesNotInterruptExecution() { using (var output = new DisconnectedStream()) { var writer = new TransactionStreamWriter(output); writer.Progress("UPDATE", "CardRequested"); writer.Deliver(); var expected = new Dictionary { { "TRANSACTION_RESULT", "Approved" } }; int starts = 0; var result = TransactionExecution.Run(() => starts++, () => Task.FromResult(expected), () => { throw new Exception("disconnect suppressed financial execution"); }, TimeSpan.FromSeconds(1)); writer.Complete(new { type = "result", result }); writer.Progress("UPDATE", "CardRequested"); Check(starts == 1 && ReferenceEquals(result, expected), "disconnect changed final transaction result"); } } private static void FinalFieldsExcludeSensitiveParameters() { var fields = TransactionStreamFields.Required(new Dictionary { { "TRANSACTION_RESULT", "Approved" }, { "PAN", "1234567890123456" }, { "PAN_MASKED", "1234567890123456" }, { "CVV", "123" }, { "PIN", "1234" }, { "TRACK_DATA", "track-secret" }, { "RAW", "raw-secret" }, { "CARD_HASH", "hash" }, { "CARD_REFERENCE", "ref" }, { "RECEIPT_DATA_CARDHOLDER", "receipt" } }); Check(!fields.ContainsKey("PAN") && !fields.ContainsKey("CVV") && !fields.ContainsKey("PIN") && !fields.ContainsKey("TRACK_DATA") && !fields.ContainsKey("RAW") && fields["PAN_MASKED"] == "", "sensitive fields crossed structured boundary"); Check(fields["CARD_HASH"] == "hash" && fields["CARD_REFERENCE"] == "ref" && fields["RECEIPT_DATA_CARDHOLDER"] == "receipt", "required final fields were lost"); Check(TransactionStreamFields.MaskedCardNumber("123456******1234") == "123456******1234" && TransactionStreamFields.MaskedCardNumber("476173******1133") == "476173******1133" && TransactionStreamFields.MaskedCardNumber("************1133") == "************1133" && TransactionStreamFields.MaskedCardNumber("1234567890123456****") == "" && TransactionStreamFields.MaskedCardNumber("34373631373330303030303031313333") == "", "valid masking rejected or encoding/full digits treated as masking"); Check(!TransactionStreamFields.IsProgress("PAN", "CardRequested") && !TransactionStreamFields.IsProgress("UPDATE", "1234567890123456") && !TransactionStreamFields.IsProgress("CARD_STATUS", "Removed"), "unsafe progress permitted"); } } }