@padosoft/react-native-ecr17 0.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/Ecr17.podspec +39 -39
  2. package/README.md +348 -348
  3. package/android/CMakeLists.txt +41 -41
  4. package/android/build.gradle +148 -148
  5. package/android/fix-prefab.gradle +50 -50
  6. package/android/gradle.properties +5 -5
  7. package/android/src/main/AndroidManifest.xml +2 -2
  8. package/android/src/main/cpp/cpp-adapter.cpp +8 -8
  9. package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -233
  10. package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -30
  11. package/cpp/Ecr17.hpp +1 -1
  12. package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -598
  13. package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -85
  14. package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -277
  15. package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -103
  16. package/cpp/Ecr17Response/Ecr17Response.cpp +155 -155
  17. package/cpp/Ecr17Response/Ecr17Response.hpp +113 -113
  18. package/cpp/Lcr/Lcr.cpp +42 -42
  19. package/cpp/Lcr/Lcr.hpp +21 -21
  20. package/cpp/PacketCodec/PacketCodec.cpp +145 -145
  21. package/cpp/PacketCodec/PacketCodec.hpp +47 -47
  22. package/cpp/Session/Ecr17Session.cpp +260 -260
  23. package/cpp/Session/Ecr17Session.hpp +97 -97
  24. package/cpp/Session/RetryPolicy.hpp +23 -23
  25. package/cpp/Transport/FakeTransport.hpp +95 -95
  26. package/cpp/Transport/NativeTransportAdapter.cpp +42 -42
  27. package/cpp/Transport/NativeTransportAdapter.hpp +32 -32
  28. package/cpp/Transport/Transport.hpp +30 -30
  29. package/cpp/tests/CMakeLists.txt +55 -55
  30. package/cpp/tests/PosixTcpTransport.hpp +105 -105
  31. package/cpp/tests/stubs/LrcMode.hpp +25 -25
  32. package/cpp/tests/test_flows.cpp +148 -148
  33. package/cpp/tests/test_integration_terminal.cpp +72 -72
  34. package/cpp/tests/test_lrc.cpp +66 -66
  35. package/cpp/tests/test_packet_codec.cpp +164 -164
  36. package/cpp/tests/test_protocol.cpp +102 -102
  37. package/cpp/tests/test_protocol_commands.cpp +190 -190
  38. package/cpp/tests/test_response.cpp +164 -164
  39. package/cpp/tests/test_retry_policy.cpp +28 -28
  40. package/cpp/tests/test_session.cpp +262 -262
  41. package/ios/HybridEcr17Transport.swift +103 -103
  42. package/lib/commonjs/index.js +50 -0
  43. package/lib/commonjs/index.js.map +1 -0
  44. package/lib/commonjs/package.json +1 -0
  45. package/lib/commonjs/specs/client.nitro.js +17 -0
  46. package/lib/commonjs/specs/client.nitro.js.map +1 -0
  47. package/lib/commonjs/specs/transport.nitro.js +6 -0
  48. package/lib/commonjs/specs/transport.nitro.js.map +1 -0
  49. package/lib/commonjs/types/client.js +2 -0
  50. package/lib/commonjs/types/client.js.map +1 -0
  51. package/lib/commonjs/utils/client.js +13 -0
  52. package/lib/commonjs/utils/client.js.map +1 -0
  53. package/lib/module/index.js +7 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/specs/client.nitro.js +13 -0
  56. package/lib/module/specs/client.nitro.js.map +1 -0
  57. package/lib/module/specs/transport.nitro.js +4 -0
  58. package/lib/module/specs/transport.nitro.js.map +1 -0
  59. package/lib/module/types/client.js +2 -0
  60. package/lib/module/types/client.js.map +1 -0
  61. package/lib/module/utils/client.js +9 -0
  62. package/lib/module/utils/client.js.map +1 -0
  63. package/lib/typescript/src/index.d.ts +5 -0
  64. package/lib/typescript/src/index.d.ts.map +1 -0
  65. package/lib/typescript/src/specs/client.nitro.d.ts +63 -0
  66. package/lib/typescript/src/specs/client.nitro.d.ts.map +1 -0
  67. package/lib/typescript/src/specs/transport.nitro.d.ts +13 -0
  68. package/lib/typescript/src/specs/transport.nitro.d.ts.map +1 -0
  69. package/lib/typescript/src/types/client.d.ts +138 -0
  70. package/lib/typescript/src/types/client.d.ts.map +1 -0
  71. package/lib/typescript/src/utils/client.d.ts +3 -0
  72. package/lib/typescript/src/utils/client.d.ts.map +1 -0
  73. package/nitro.json +30 -30
  74. package/package.json +4 -4
  75. package/react-native.config.js +18 -18
  76. package/src/index.ts +4 -4
  77. package/src/specs/client.nitro.ts +102 -102
  78. package/src/specs/transport.nitro.ts +25 -25
  79. package/src/types/client.ts +196 -196
  80. package/src/utils/client.ts +10 -10
@@ -1,262 +1,262 @@
1
- // Orchestration tests for Ecr17Session driven by the in-memory FakeTransport:
2
- // ACK/NAK handshake, retransmission, timeouts, LRC-failure NAK, and progress /
3
- // receipt forwarding. Timeouts are tiny so the suite stays fast.
4
-
5
- #include <gtest/gtest.h>
6
-
7
- #include <cstdint>
8
- #include <string>
9
- #include <vector>
10
-
11
- #include "PacketCodec/PacketCodec.hpp"
12
- #include "Session/Ecr17Session.hpp"
13
- #include "Transport/FakeTransport.hpp"
14
-
15
- using namespace margelo::nitro::ecr17;
16
-
17
- namespace {
18
-
19
- SessionConfig fastConfig() {
20
- SessionConfig c;
21
- c.lrcMode = LrcMode::STD;
22
- c.ackTimeoutMs = 40;
23
- c.responseTimeoutMs = 40;
24
- c.retryCount = 2;
25
- c.retryDelayMs = 1;
26
- return c;
27
- }
28
-
29
- void append(std::vector<uint8_t>& a, const std::vector<uint8_t>& b) {
30
- a.insert(a.end(), b.begin(), b.end());
31
- }
32
-
33
- std::vector<uint8_t> progressFrame(const std::string& msg20) {
34
- std::vector<uint8_t> f{0x01}; // SOH
35
- f.insert(f.end(), msg20.begin(), msg20.end());
36
- f.push_back(0x04); // EOT
37
- return f;
38
- }
39
-
40
- const std::string kResultPayload = "123456780E0000DATA"; // code 'E' at pos 10 -> result
41
- const std::string kReceiptPayload = "123456780SLINE 1"; // code 'S' at pos 10 -> receipt
42
-
43
- } // namespace
44
-
45
- TEST(Session, HappyPathReturnsResultAndAcks) {
46
- FakeTransport t;
47
- PacketCodec codec(LrcMode::STD);
48
- std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
49
- append(response, codec.encodeApplication(kResultPayload));
50
- t.enqueueResponse(response);
51
-
52
- Ecr17Session session(t, fastConfig());
53
- DecodedPacket result = session.exchange("123456780P...");
54
- EXPECT_EQ(result.type, PacketType::APPLICATION);
55
- EXPECT_TRUE(result.validLrc);
56
- EXPECT_EQ(result.payload, kResultPayload);
57
- EXPECT_EQ(t.applicationRequestCount(), 1u);
58
-
59
- // The session must have ACKed the received result frame.
60
- bool sentAck = false;
61
- for (const auto& f : t.sentFrames()) {
62
- if (!f.empty() && f.front() == PacketCodec::ACK) sentAck = true;
63
- }
64
- EXPECT_TRUE(sentAck);
65
- }
66
-
67
- TEST(Session, NakTriggersRetransmitThenSucceeds) {
68
- FakeTransport t;
69
- PacketCodec codec(LrcMode::STD);
70
- t.enqueueResponse(codec.encodeControl(PacketCodec::NAK)); // reply to attempt 1
71
- std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
72
- append(ok, codec.encodeApplication(kResultPayload));
73
- t.enqueueResponse(ok); // reply to retransmit
74
-
75
- Ecr17Session session(t, fastConfig());
76
- DecodedPacket result = session.exchange("123456780P...");
77
- EXPECT_EQ(result.payload, kResultPayload);
78
- EXPECT_EQ(t.applicationRequestCount(), 2u); // initial + 1 retransmit
79
- }
80
-
81
- TEST(Session, AckTimeoutExhaustsRetriesThenThrows) {
82
- FakeTransport t; // no responses queued
83
- Ecr17Session session(t, fastConfig());
84
- EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
85
- // initial + retryCount retransmissions
86
- EXPECT_EQ(t.applicationRequestCount(), 3u);
87
- }
88
-
89
- TEST(Session, BadLrcResponseSendsNak) {
90
- FakeTransport t;
91
- PacketCodec codec(LrcMode::STD);
92
- std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
93
- auto bad = codec.encodeApplication(kResultPayload);
94
- bad.back() ^= 0xFF; // corrupt LRC
95
- append(response, bad);
96
- t.enqueueResponse(response);
97
-
98
- Ecr17Session session(t, fastConfig());
99
- EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error); // no valid result
100
-
101
- bool sentNak = false;
102
- for (const auto& f : t.sentFrames()) {
103
- if (!f.empty() && f.front() == PacketCodec::NAK) sentNak = true;
104
- }
105
- EXPECT_TRUE(sentNak);
106
- }
107
-
108
- TEST(Session, ProgressMessagesForwarded) {
109
- FakeTransport t;
110
- PacketCodec codec(LrcMode::STD);
111
- std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
112
- append(response, progressFrame("ATTENDERE PREGO "));
113
- append(response, codec.encodeApplication(kResultPayload));
114
- t.enqueueResponse(response);
115
-
116
- std::vector<std::string> progress;
117
- Ecr17Session session(t, fastConfig());
118
- session.setOnProgress([&](const std::string& m) { progress.push_back(m); });
119
-
120
- DecodedPacket result = session.exchange("123456780P...");
121
- EXPECT_EQ(result.payload, kResultPayload);
122
- ASSERT_EQ(progress.size(), 1u);
123
- EXPECT_EQ(progress[0], "ATTENDERE PREGO ");
124
- }
125
-
126
- TEST(Session, ReceiptLinesForwardedBeforeResult) {
127
- FakeTransport t;
128
- PacketCodec codec(LrcMode::STD);
129
- std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
130
- append(response, codec.encodeApplication(kReceiptPayload));
131
- append(response, codec.encodeApplication(kResultPayload));
132
- t.enqueueResponse(response);
133
-
134
- std::vector<std::string> receipts;
135
- Ecr17Session session(t, fastConfig());
136
- session.setOnReceiptLine([&](const std::string& l) { receipts.push_back(l); });
137
-
138
- DecodedPacket result = session.exchange("123456780P...");
139
- EXPECT_EQ(result.payload, kResultPayload);
140
- ASSERT_EQ(receipts.size(), 1u);
141
- EXPECT_EQ(receipts[0], kReceiptPayload);
142
- }
143
-
144
- // Regression: some terminals send the application RESULT before (or instead of)
145
- // the physical ACK. The handshake must not discard that frame — losing the
146
- // result of a completed financial transaction is the worst failure mode.
147
- TEST(Session, ResultBeforeAckIsNotLost) {
148
- FakeTransport t;
149
- PacketCodec codec(LrcMode::STD);
150
- t.enqueueResponse(codec.encodeApplication(kResultPayload)); // result, no leading ACK
151
-
152
- Ecr17Session session(t, fastConfig());
153
- DecodedPacket result = session.exchange("123456780P...");
154
- EXPECT_EQ(result.type, PacketType::APPLICATION);
155
- EXPECT_EQ(result.payload, kResultPayload);
156
- EXPECT_EQ(t.applicationRequestCount(), 1u); // no spurious retransmit
157
-
158
- // The accepted result must still be ACKed back to the terminal.
159
- bool sentAck = false;
160
- for (const auto& f : t.sentFrames()) {
161
- if (!f.empty() && f.front() == PacketCodec::ACK) sentAck = true;
162
- }
163
- EXPECT_TRUE(sentAck);
164
- }
165
-
166
- TEST(Session, ResponseTimeoutAfterAckThrows) {
167
- FakeTransport t;
168
- PacketCodec codec(LrcMode::STD);
169
- t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK only, no result
170
-
171
- Ecr17Session session(t, fastConfig());
172
- EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
173
- }
174
-
175
- TEST(Session, DisconnectDuringExchangeThrows) {
176
- FakeTransport t;
177
- t.disconnectOnNextRequest();
178
- Ecr17Session session(t, fastConfig());
179
- EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
180
- }
181
-
182
- // Scenario: the connection drops mid-exchange, then the client reconnects and
183
- // retries (the auto-reconnect path for safe commands). The session must recover
184
- // — it must NOT stay stuck in the disconnected state after a drop.
185
- TEST(Session, RecoversAndSucceedsAfterReconnect) {
186
- FakeTransport t;
187
- t.disconnectOnNextRequest();
188
- Ecr17Session session(t, fastConfig());
189
-
190
- // First attempt drops.
191
- EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
192
-
193
- // Client reconnects the transport and the next transaction must work.
194
- t.rearm();
195
- PacketCodec codec(LrcMode::STD);
196
- std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
197
- append(ok, codec.encodeApplication(kResultPayload));
198
- t.enqueueResponse(ok);
199
-
200
- DecodedPacket result = session.exchange("123456780P...");
201
- EXPECT_EQ(result.type, PacketType::APPLICATION);
202
- EXPECT_EQ(result.payload, kResultPayload);
203
- }
204
-
205
- TEST(Session, SendAckOnlyReturnsOnAck) {
206
- FakeTransport t;
207
- PacketCodec codec(LrcMode::STD);
208
- t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK, no app result
209
- Ecr17Session session(t, fastConfig());
210
- EXPECT_NO_THROW(session.sendAckOnly("123456780E1"));
211
- EXPECT_EQ(t.applicationRequestCount(), 1u);
212
- }
213
-
214
- TEST(Session, SendAckOnlyRetransmitsOnNak) {
215
- FakeTransport t;
216
- PacketCodec codec(LrcMode::STD);
217
- t.enqueueResponse(codec.encodeControl(PacketCodec::NAK));
218
- t.enqueueResponse(codec.encodeControl(PacketCodec::ACK));
219
- Ecr17Session session(t, fastConfig());
220
- EXPECT_NO_THROW(session.sendAckOnly("123456780E0"));
221
- EXPECT_EQ(t.applicationRequestCount(), 2u);
222
- }
223
-
224
- TEST(Session, SendAckOnlyTimesOut) {
225
- FakeTransport t; // no ACK
226
- Ecr17Session session(t, fastConfig());
227
- EXPECT_THROW(session.sendAckOnly("123456780E1"), std::runtime_error);
228
- }
229
-
230
- TEST(Session, ExchangeWithAdditionalDataSendsTwoRequests) {
231
- FakeTransport t;
232
- PacketCodec codec(LrcMode::STD);
233
- t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK for the main 'P'
234
- std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
235
- append(ok, codec.encodeApplication(kResultPayload));
236
- t.enqueueResponse(ok); // ACK for 'U' + the result
237
-
238
- Ecr17Session session(t, fastConfig());
239
- DecodedPacket result = session.exchangeWithAdditionalData("123456780P...", "123456780U...");
240
- EXPECT_EQ(result.payload, kResultPayload);
241
- EXPECT_EQ(t.applicationRequestCount(), 2u); // P + U
242
- }
243
-
244
- TEST(Session, ReceiptDrainForwardsReceiptsAfterResult) {
245
- FakeTransport t;
246
- PacketCodec codec(LrcMode::STD);
247
- std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
248
- append(response, codec.encodeApplication(kResultPayload)); // result first
249
- append(response, codec.encodeApplication(kReceiptPayload)); // then a receipt line
250
- t.enqueueResponse(response);
251
-
252
- SessionConfig cfg = fastConfig();
253
- cfg.receiptDrainMs = 30; // drain receipts that follow the result
254
- std::vector<std::string> receipts;
255
- Ecr17Session session(t, cfg);
256
- session.setOnReceiptLine([&](const std::string& l) { receipts.push_back(l); });
257
-
258
- DecodedPacket result = session.exchange("123456780P...");
259
- EXPECT_EQ(result.payload, kResultPayload);
260
- ASSERT_EQ(receipts.size(), 1u);
261
- EXPECT_EQ(receipts[0], kReceiptPayload);
262
- }
1
+ // Orchestration tests for Ecr17Session driven by the in-memory FakeTransport:
2
+ // ACK/NAK handshake, retransmission, timeouts, LRC-failure NAK, and progress /
3
+ // receipt forwarding. Timeouts are tiny so the suite stays fast.
4
+
5
+ #include <gtest/gtest.h>
6
+
7
+ #include <cstdint>
8
+ #include <string>
9
+ #include <vector>
10
+
11
+ #include "PacketCodec/PacketCodec.hpp"
12
+ #include "Session/Ecr17Session.hpp"
13
+ #include "Transport/FakeTransport.hpp"
14
+
15
+ using namespace margelo::nitro::ecr17;
16
+
17
+ namespace {
18
+
19
+ SessionConfig fastConfig() {
20
+ SessionConfig c;
21
+ c.lrcMode = LrcMode::STD;
22
+ c.ackTimeoutMs = 40;
23
+ c.responseTimeoutMs = 40;
24
+ c.retryCount = 2;
25
+ c.retryDelayMs = 1;
26
+ return c;
27
+ }
28
+
29
+ void append(std::vector<uint8_t>& a, const std::vector<uint8_t>& b) {
30
+ a.insert(a.end(), b.begin(), b.end());
31
+ }
32
+
33
+ std::vector<uint8_t> progressFrame(const std::string& msg20) {
34
+ std::vector<uint8_t> f{0x01}; // SOH
35
+ f.insert(f.end(), msg20.begin(), msg20.end());
36
+ f.push_back(0x04); // EOT
37
+ return f;
38
+ }
39
+
40
+ const std::string kResultPayload = "123456780E0000DATA"; // code 'E' at pos 10 -> result
41
+ const std::string kReceiptPayload = "123456780SLINE 1"; // code 'S' at pos 10 -> receipt
42
+
43
+ } // namespace
44
+
45
+ TEST(Session, HappyPathReturnsResultAndAcks) {
46
+ FakeTransport t;
47
+ PacketCodec codec(LrcMode::STD);
48
+ std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
49
+ append(response, codec.encodeApplication(kResultPayload));
50
+ t.enqueueResponse(response);
51
+
52
+ Ecr17Session session(t, fastConfig());
53
+ DecodedPacket result = session.exchange("123456780P...");
54
+ EXPECT_EQ(result.type, PacketType::APPLICATION);
55
+ EXPECT_TRUE(result.validLrc);
56
+ EXPECT_EQ(result.payload, kResultPayload);
57
+ EXPECT_EQ(t.applicationRequestCount(), 1u);
58
+
59
+ // The session must have ACKed the received result frame.
60
+ bool sentAck = false;
61
+ for (const auto& f : t.sentFrames()) {
62
+ if (!f.empty() && f.front() == PacketCodec::ACK) sentAck = true;
63
+ }
64
+ EXPECT_TRUE(sentAck);
65
+ }
66
+
67
+ TEST(Session, NakTriggersRetransmitThenSucceeds) {
68
+ FakeTransport t;
69
+ PacketCodec codec(LrcMode::STD);
70
+ t.enqueueResponse(codec.encodeControl(PacketCodec::NAK)); // reply to attempt 1
71
+ std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
72
+ append(ok, codec.encodeApplication(kResultPayload));
73
+ t.enqueueResponse(ok); // reply to retransmit
74
+
75
+ Ecr17Session session(t, fastConfig());
76
+ DecodedPacket result = session.exchange("123456780P...");
77
+ EXPECT_EQ(result.payload, kResultPayload);
78
+ EXPECT_EQ(t.applicationRequestCount(), 2u); // initial + 1 retransmit
79
+ }
80
+
81
+ TEST(Session, AckTimeoutExhaustsRetriesThenThrows) {
82
+ FakeTransport t; // no responses queued
83
+ Ecr17Session session(t, fastConfig());
84
+ EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
85
+ // initial + retryCount retransmissions
86
+ EXPECT_EQ(t.applicationRequestCount(), 3u);
87
+ }
88
+
89
+ TEST(Session, BadLrcResponseSendsNak) {
90
+ FakeTransport t;
91
+ PacketCodec codec(LrcMode::STD);
92
+ std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
93
+ auto bad = codec.encodeApplication(kResultPayload);
94
+ bad.back() ^= 0xFF; // corrupt LRC
95
+ append(response, bad);
96
+ t.enqueueResponse(response);
97
+
98
+ Ecr17Session session(t, fastConfig());
99
+ EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error); // no valid result
100
+
101
+ bool sentNak = false;
102
+ for (const auto& f : t.sentFrames()) {
103
+ if (!f.empty() && f.front() == PacketCodec::NAK) sentNak = true;
104
+ }
105
+ EXPECT_TRUE(sentNak);
106
+ }
107
+
108
+ TEST(Session, ProgressMessagesForwarded) {
109
+ FakeTransport t;
110
+ PacketCodec codec(LrcMode::STD);
111
+ std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
112
+ append(response, progressFrame("ATTENDERE PREGO "));
113
+ append(response, codec.encodeApplication(kResultPayload));
114
+ t.enqueueResponse(response);
115
+
116
+ std::vector<std::string> progress;
117
+ Ecr17Session session(t, fastConfig());
118
+ session.setOnProgress([&](const std::string& m) { progress.push_back(m); });
119
+
120
+ DecodedPacket result = session.exchange("123456780P...");
121
+ EXPECT_EQ(result.payload, kResultPayload);
122
+ ASSERT_EQ(progress.size(), 1u);
123
+ EXPECT_EQ(progress[0], "ATTENDERE PREGO ");
124
+ }
125
+
126
+ TEST(Session, ReceiptLinesForwardedBeforeResult) {
127
+ FakeTransport t;
128
+ PacketCodec codec(LrcMode::STD);
129
+ std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
130
+ append(response, codec.encodeApplication(kReceiptPayload));
131
+ append(response, codec.encodeApplication(kResultPayload));
132
+ t.enqueueResponse(response);
133
+
134
+ std::vector<std::string> receipts;
135
+ Ecr17Session session(t, fastConfig());
136
+ session.setOnReceiptLine([&](const std::string& l) { receipts.push_back(l); });
137
+
138
+ DecodedPacket result = session.exchange("123456780P...");
139
+ EXPECT_EQ(result.payload, kResultPayload);
140
+ ASSERT_EQ(receipts.size(), 1u);
141
+ EXPECT_EQ(receipts[0], kReceiptPayload);
142
+ }
143
+
144
+ // Regression: some terminals send the application RESULT before (or instead of)
145
+ // the physical ACK. The handshake must not discard that frame — losing the
146
+ // result of a completed financial transaction is the worst failure mode.
147
+ TEST(Session, ResultBeforeAckIsNotLost) {
148
+ FakeTransport t;
149
+ PacketCodec codec(LrcMode::STD);
150
+ t.enqueueResponse(codec.encodeApplication(kResultPayload)); // result, no leading ACK
151
+
152
+ Ecr17Session session(t, fastConfig());
153
+ DecodedPacket result = session.exchange("123456780P...");
154
+ EXPECT_EQ(result.type, PacketType::APPLICATION);
155
+ EXPECT_EQ(result.payload, kResultPayload);
156
+ EXPECT_EQ(t.applicationRequestCount(), 1u); // no spurious retransmit
157
+
158
+ // The accepted result must still be ACKed back to the terminal.
159
+ bool sentAck = false;
160
+ for (const auto& f : t.sentFrames()) {
161
+ if (!f.empty() && f.front() == PacketCodec::ACK) sentAck = true;
162
+ }
163
+ EXPECT_TRUE(sentAck);
164
+ }
165
+
166
+ TEST(Session, ResponseTimeoutAfterAckThrows) {
167
+ FakeTransport t;
168
+ PacketCodec codec(LrcMode::STD);
169
+ t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK only, no result
170
+
171
+ Ecr17Session session(t, fastConfig());
172
+ EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
173
+ }
174
+
175
+ TEST(Session, DisconnectDuringExchangeThrows) {
176
+ FakeTransport t;
177
+ t.disconnectOnNextRequest();
178
+ Ecr17Session session(t, fastConfig());
179
+ EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
180
+ }
181
+
182
+ // Scenario: the connection drops mid-exchange, then the client reconnects and
183
+ // retries (the auto-reconnect path for safe commands). The session must recover
184
+ // — it must NOT stay stuck in the disconnected state after a drop.
185
+ TEST(Session, RecoversAndSucceedsAfterReconnect) {
186
+ FakeTransport t;
187
+ t.disconnectOnNextRequest();
188
+ Ecr17Session session(t, fastConfig());
189
+
190
+ // First attempt drops.
191
+ EXPECT_THROW(session.exchange("123456780P..."), std::runtime_error);
192
+
193
+ // Client reconnects the transport and the next transaction must work.
194
+ t.rearm();
195
+ PacketCodec codec(LrcMode::STD);
196
+ std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
197
+ append(ok, codec.encodeApplication(kResultPayload));
198
+ t.enqueueResponse(ok);
199
+
200
+ DecodedPacket result = session.exchange("123456780P...");
201
+ EXPECT_EQ(result.type, PacketType::APPLICATION);
202
+ EXPECT_EQ(result.payload, kResultPayload);
203
+ }
204
+
205
+ TEST(Session, SendAckOnlyReturnsOnAck) {
206
+ FakeTransport t;
207
+ PacketCodec codec(LrcMode::STD);
208
+ t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK, no app result
209
+ Ecr17Session session(t, fastConfig());
210
+ EXPECT_NO_THROW(session.sendAckOnly("123456780E1"));
211
+ EXPECT_EQ(t.applicationRequestCount(), 1u);
212
+ }
213
+
214
+ TEST(Session, SendAckOnlyRetransmitsOnNak) {
215
+ FakeTransport t;
216
+ PacketCodec codec(LrcMode::STD);
217
+ t.enqueueResponse(codec.encodeControl(PacketCodec::NAK));
218
+ t.enqueueResponse(codec.encodeControl(PacketCodec::ACK));
219
+ Ecr17Session session(t, fastConfig());
220
+ EXPECT_NO_THROW(session.sendAckOnly("123456780E0"));
221
+ EXPECT_EQ(t.applicationRequestCount(), 2u);
222
+ }
223
+
224
+ TEST(Session, SendAckOnlyTimesOut) {
225
+ FakeTransport t; // no ACK
226
+ Ecr17Session session(t, fastConfig());
227
+ EXPECT_THROW(session.sendAckOnly("123456780E1"), std::runtime_error);
228
+ }
229
+
230
+ TEST(Session, ExchangeWithAdditionalDataSendsTwoRequests) {
231
+ FakeTransport t;
232
+ PacketCodec codec(LrcMode::STD);
233
+ t.enqueueResponse(codec.encodeControl(PacketCodec::ACK)); // ACK for the main 'P'
234
+ std::vector<uint8_t> ok = codec.encodeControl(PacketCodec::ACK);
235
+ append(ok, codec.encodeApplication(kResultPayload));
236
+ t.enqueueResponse(ok); // ACK for 'U' + the result
237
+
238
+ Ecr17Session session(t, fastConfig());
239
+ DecodedPacket result = session.exchangeWithAdditionalData("123456780P...", "123456780U...");
240
+ EXPECT_EQ(result.payload, kResultPayload);
241
+ EXPECT_EQ(t.applicationRequestCount(), 2u); // P + U
242
+ }
243
+
244
+ TEST(Session, ReceiptDrainForwardsReceiptsAfterResult) {
245
+ FakeTransport t;
246
+ PacketCodec codec(LrcMode::STD);
247
+ std::vector<uint8_t> response = codec.encodeControl(PacketCodec::ACK);
248
+ append(response, codec.encodeApplication(kResultPayload)); // result first
249
+ append(response, codec.encodeApplication(kReceiptPayload)); // then a receipt line
250
+ t.enqueueResponse(response);
251
+
252
+ SessionConfig cfg = fastConfig();
253
+ cfg.receiptDrainMs = 30; // drain receipts that follow the result
254
+ std::vector<std::string> receipts;
255
+ Ecr17Session session(t, cfg);
256
+ session.setOnReceiptLine([&](const std::string& l) { receipts.push_back(l); });
257
+
258
+ DecodedPacket result = session.exchange("123456780P...");
259
+ EXPECT_EQ(result.payload, kResultPayload);
260
+ ASSERT_EQ(receipts.size(), 1u);
261
+ EXPECT_EQ(receipts[0], kReceiptPayload);
262
+ }