@padosoft/react-native-ecr17 0.0.0
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.
- package/Ecr17.podspec +39 -0
- package/README.md +348 -0
- package/android/CMakeLists.txt +41 -0
- package/android/build.gradle +149 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +9 -0
- package/android/src/main/java/com/margelo/nitro/ecr17/HybridEcr17Transport.kt +233 -0
- package/android/src/main/java/com/padosoft/ecr17/Ecr17Package.kt +30 -0
- package/cpp/Ecr17.cpp +1 -0
- package/cpp/Ecr17.hpp +2 -0
- package/cpp/Ecr17Client/HybridEcr17Client.cpp +598 -0
- package/cpp/Ecr17Client/HybridEcr17Client.hpp +85 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.cpp +277 -0
- package/cpp/Ecr17Protocol/Ecr17Protocol.hpp +103 -0
- package/cpp/Ecr17Response/Ecr17Response.cpp +155 -0
- package/cpp/Ecr17Response/Ecr17Response.hpp +113 -0
- package/cpp/Lcr/Lcr.cpp +42 -0
- package/cpp/Lcr/Lcr.hpp +22 -0
- package/cpp/PacketCodec/PacketCodec.cpp +146 -0
- package/cpp/PacketCodec/PacketCodec.hpp +48 -0
- package/cpp/Session/Ecr17Session.cpp +260 -0
- package/cpp/Session/Ecr17Session.hpp +97 -0
- package/cpp/Session/RetryPolicy.hpp +23 -0
- package/cpp/Transport/FakeTransport.hpp +95 -0
- package/cpp/Transport/NativeTransportAdapter.cpp +42 -0
- package/cpp/Transport/NativeTransportAdapter.hpp +32 -0
- package/cpp/Transport/Transport.hpp +31 -0
- package/cpp/tests/CMakeLists.txt +55 -0
- package/cpp/tests/PosixTcpTransport.hpp +105 -0
- package/cpp/tests/stubs/LrcMode.hpp +25 -0
- package/cpp/tests/test_flows.cpp +148 -0
- package/cpp/tests/test_integration_terminal.cpp +72 -0
- package/cpp/tests/test_lrc.cpp +66 -0
- package/cpp/tests/test_packet_codec.cpp +164 -0
- package/cpp/tests/test_protocol.cpp +102 -0
- package/cpp/tests/test_protocol_commands.cpp +190 -0
- package/cpp/tests/test_response.cpp +164 -0
- package/cpp/tests/test_retry_policy.cpp +28 -0
- package/cpp/tests/test_session.cpp +262 -0
- package/ios/Bridge.h +1 -0
- package/ios/HybridEcr17Transport.swift +103 -0
- package/nitro.json +30 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/Ecr17+autolinking.cmake +82 -0
- package/nitrogen/generated/android/Ecr17+autolinking.gradle +27 -0
- package/nitrogen/generated/android/Ecr17OnLoad.cpp +68 -0
- package/nitrogen/generated/android/Ecr17OnLoad.hpp +34 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +75 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__shared_ptr_ArrayBuffer_.hpp +77 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.cpp +93 -0
- package/nitrogen/generated/android/c++/JHybridEcr17TransportSpec.hpp +68 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Ecr17OnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/Func_void_std__shared_ptr_ArrayBuffer_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/ecr17/HybridEcr17TransportSpec.kt +86 -0
- package/nitrogen/generated/ios/Ecr17+autolinking.rb +62 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.cpp +57 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Bridge.hpp +154 -0
- package/nitrogen/generated/ios/Ecr17-Swift-Cxx-Umbrella.hpp +47 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.mm +43 -0
- package/nitrogen/generated/ios/Ecr17Autolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridEcr17TransportSpecSwift.hpp +119 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_ArrayBuffer_.swift +46 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridEcr17TransportSpec_cxx.swift +211 -0
- package/nitrogen/generated/shared/c++/CardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/CardVerificationRequest.hpp +97 -0
- package/nitrogen/generated/shared/c++/CardVerificationResult.hpp +136 -0
- package/nitrogen/generated/shared/c++/CloseSessionResult.hpp +106 -0
- package/nitrogen/generated/shared/c++/ConnectionState.hpp +80 -0
- package/nitrogen/generated/shared/c++/CurrencyExchange.hpp +100 -0
- package/nitrogen/generated/shared/c++/Ecr17Config.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.cpp +42 -0
- package/nitrogen/generated/shared/c++/HybridEcr17ClientSpec.hpp +138 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridEcr17TransportSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/IncrementalAuthRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/LrcMode.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentCardType.hpp +84 -0
- package/nitrogen/generated/shared/c++/PaymentRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PaymentResult.hpp +139 -0
- package/nitrogen/generated/shared/c++/PosStatusResponse.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthClosureRequest.hpp +96 -0
- package/nitrogen/generated/shared/c++/PreAuthRequest.hpp +109 -0
- package/nitrogen/generated/shared/c++/PreAuthResult.hpp +144 -0
- package/nitrogen/generated/shared/c++/ProgressEvent.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReceiptLine.hpp +83 -0
- package/nitrogen/generated/shared/c++/ReversalRequest.hpp +88 -0
- package/nitrogen/generated/shared/c++/ReversalResult.hpp +132 -0
- package/nitrogen/generated/shared/c++/TokenizationRequest.hpp +89 -0
- package/nitrogen/generated/shared/c++/TokenizationService.hpp +76 -0
- package/nitrogen/generated/shared/c++/TotalsResult.hpp +93 -0
- package/nitrogen/generated/shared/c++/TransactionEntryMode.hpp +92 -0
- package/nitrogen/generated/shared/c++/TransactionOutcome.hpp +88 -0
- package/nitrogen/generated/shared/c++/VasResult.hpp +96 -0
- package/package.json +102 -0
- package/react-native.config.js +18 -0
- package/src/index.ts +4 -0
- package/src/specs/client.nitro.ts +102 -0
- package/src/specs/transport.nitro.ts +25 -0
- package/src/types/client.ts +196 -0
- package/src/utils/client.ts +10 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#include <gtest/gtest.h>
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <string>
|
|
5
|
+
#include <vector>
|
|
6
|
+
|
|
7
|
+
#include "PacketCodec/PacketCodec.hpp"
|
|
8
|
+
|
|
9
|
+
using margelo::nitro::ecr17::DecodedPacket;
|
|
10
|
+
using margelo::nitro::ecr17::LrcMode;
|
|
11
|
+
using margelo::nitro::ecr17::PacketCodec;
|
|
12
|
+
using margelo::nitro::ecr17::PacketType;
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
constexpr uint8_t kSoh = 0x01;
|
|
16
|
+
constexpr uint8_t kStx = 0x02;
|
|
17
|
+
constexpr uint8_t kEtx = 0x03;
|
|
18
|
+
constexpr uint8_t kEot = 0x04;
|
|
19
|
+
constexpr uint8_t kAck = 0x06;
|
|
20
|
+
constexpr uint8_t kNak = 0x15;
|
|
21
|
+
} // namespace
|
|
22
|
+
|
|
23
|
+
TEST(PacketCodec, EncodeApplicationFramesStxPayloadEtxLrc) {
|
|
24
|
+
PacketCodec codec(LrcMode::STD);
|
|
25
|
+
auto frame = codec.encodeApplication("AB");
|
|
26
|
+
ASSERT_EQ(frame.size(), 5u);
|
|
27
|
+
EXPECT_EQ(frame[0], kStx);
|
|
28
|
+
EXPECT_EQ(frame[1], 'A');
|
|
29
|
+
EXPECT_EQ(frame[2], 'B');
|
|
30
|
+
EXPECT_EQ(frame[3], kEtx);
|
|
31
|
+
EXPECT_EQ(frame[4], 0x7C); // 0x7F ^ 'A' ^ 'B'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
TEST(PacketCodec, ApplicationRoundTrip) {
|
|
35
|
+
for (LrcMode mode : {LrcMode::STX, LrcMode::STD, LrcMode::NOEXT, LrcMode::STX_NOEXT}) {
|
|
36
|
+
PacketCodec codec(mode);
|
|
37
|
+
const std::string payload = "123456780P0000065000";
|
|
38
|
+
DecodedPacket decoded = codec.decode(codec.encodeApplication(payload));
|
|
39
|
+
EXPECT_EQ(decoded.type, PacketType::APPLICATION);
|
|
40
|
+
EXPECT_EQ(decoded.payload, payload);
|
|
41
|
+
EXPECT_TRUE(decoded.validLrc);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
TEST(PacketCodec, ApplicationDetectsCorruptedLrc) {
|
|
46
|
+
PacketCodec codec(LrcMode::STD);
|
|
47
|
+
auto frame = codec.encodeApplication("HELLO");
|
|
48
|
+
frame.back() ^= 0xFF; // corrupt the LRC byte
|
|
49
|
+
DecodedPacket decoded = codec.decode(frame);
|
|
50
|
+
EXPECT_EQ(decoded.type, PacketType::APPLICATION);
|
|
51
|
+
EXPECT_EQ(decoded.payload, "HELLO");
|
|
52
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
TEST(PacketCodec, EncodeControlFramesCtrlEtxLrc) {
|
|
56
|
+
PacketCodec codec(LrcMode::STD);
|
|
57
|
+
auto frame = codec.encodeControl(kAck);
|
|
58
|
+
ASSERT_EQ(frame.size(), 3u);
|
|
59
|
+
EXPECT_EQ(frame[0], kAck);
|
|
60
|
+
EXPECT_EQ(frame[1], kEtx);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
TEST(PacketCodec, DecodeAck) {
|
|
64
|
+
PacketCodec codec(LrcMode::STD);
|
|
65
|
+
DecodedPacket decoded = codec.decode({kAck});
|
|
66
|
+
EXPECT_EQ(decoded.type, PacketType::ACK);
|
|
67
|
+
EXPECT_TRUE(decoded.validLrc);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
TEST(PacketCodec, DecodeNak) {
|
|
71
|
+
PacketCodec codec(LrcMode::STD);
|
|
72
|
+
DecodedPacket decoded = codec.decode({kNak});
|
|
73
|
+
EXPECT_EQ(decoded.type, PacketType::NAK);
|
|
74
|
+
EXPECT_TRUE(decoded.validLrc);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
TEST(PacketCodec, DecodeEmptyIsUnknown) {
|
|
78
|
+
PacketCodec codec(LrcMode::STD);
|
|
79
|
+
DecodedPacket decoded = codec.decode({});
|
|
80
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
81
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Regression: a lone SOH byte previously built a string from an inverted
|
|
85
|
+
// iterator range [begin()+1, end()-1) == [end(), begin()) -> UB/crash.
|
|
86
|
+
TEST(PacketCodec, DecodeLoneSohIsUnknownNotCrash) {
|
|
87
|
+
PacketCodec codec(LrcMode::STD);
|
|
88
|
+
DecodedPacket decoded = codec.decode({kSoh});
|
|
89
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
90
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
TEST(PacketCodec, DecodeProgressUpdate) {
|
|
94
|
+
PacketCodec codec(LrcMode::STD);
|
|
95
|
+
std::vector<uint8_t> frame{kSoh};
|
|
96
|
+
const std::string msg = "ELABORAZIONE... "; // 20 chars per spec
|
|
97
|
+
frame.insert(frame.end(), msg.begin(), msg.end());
|
|
98
|
+
frame.push_back(kEot);
|
|
99
|
+
|
|
100
|
+
DecodedPacket decoded = codec.decode(frame);
|
|
101
|
+
EXPECT_EQ(decoded.type, PacketType::PROGRESS);
|
|
102
|
+
EXPECT_EQ(decoded.payload, msg);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
TEST(PacketCodec, DecodeStxWithoutEtxIsUnknown) {
|
|
106
|
+
PacketCodec codec(LrcMode::STD);
|
|
107
|
+
DecodedPacket decoded = codec.decode({kStx, 'A', 'B'});
|
|
108
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
109
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Regression: ETX present but no trailing LRC byte must not read past the end
|
|
113
|
+
// nor mistake ETX for the LRC.
|
|
114
|
+
TEST(PacketCodec, DecodeStxWithEtxButNoLrcIsUnknown) {
|
|
115
|
+
PacketCodec codec(LrcMode::STD);
|
|
116
|
+
DecodedPacket decoded = codec.decode({kStx, 'A', kEtx});
|
|
117
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
118
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
TEST(PacketCodec, DecodeUnknownLeadByte) {
|
|
122
|
+
PacketCodec codec(LrcMode::STD);
|
|
123
|
+
DecodedPacket decoded = codec.decode({0x99, 0x00});
|
|
124
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
125
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Regression: trailing bytes after a complete frame's LRC must not be silently
|
|
129
|
+
// accepted (the LRC must be the final byte).
|
|
130
|
+
TEST(PacketCodec, DecodeStxWithTrailingBytesAfterLrcIsUnknown) {
|
|
131
|
+
PacketCodec codec(LrcMode::STD);
|
|
132
|
+
auto frame = codec.encodeApplication("AB"); // STX A B ETX LRC
|
|
133
|
+
frame.push_back(0x00); // stray trailing byte
|
|
134
|
+
DecodedPacket decoded = codec.decode(frame);
|
|
135
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
136
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Regression: a coalesced read holding two frames must not be reported as one
|
|
140
|
+
// valid APPLICATION packet (which would silently drop the second frame).
|
|
141
|
+
// Framing/splitting a byte stream is the transport layer's job.
|
|
142
|
+
TEST(PacketCodec, DecodeCoalescedFramesIsUnknown) {
|
|
143
|
+
PacketCodec codec(LrcMode::STD);
|
|
144
|
+
auto first = codec.encodeApplication("AB");
|
|
145
|
+
auto second = codec.encodeApplication("CD");
|
|
146
|
+
first.insert(first.end(), second.begin(), second.end());
|
|
147
|
+
DecodedPacket decoded = codec.decode(first);
|
|
148
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
149
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Regression: SOH frame whose last byte is not EOT must not be accepted as a
|
|
153
|
+
// valid PROGRESS packet. Only SOH + payload + EOT is well-formed per spec.
|
|
154
|
+
TEST(PacketCodec, DecodeSohWithoutEotIsUnknown) {
|
|
155
|
+
PacketCodec codec(LrcMode::STD);
|
|
156
|
+
// SOH + 20-char message, but terminated with 0xFF instead of EOT.
|
|
157
|
+
std::vector<uint8_t> frame{kSoh};
|
|
158
|
+
const std::string msg = "ELABORAZIONE... ";
|
|
159
|
+
frame.insert(frame.end(), msg.begin(), msg.end());
|
|
160
|
+
frame.push_back(0xFF); // wrong terminator
|
|
161
|
+
DecodedPacket decoded = codec.decode(frame);
|
|
162
|
+
EXPECT_EQ(decoded.type, PacketType::UNKNOWN);
|
|
163
|
+
EXPECT_FALSE(decoded.validLrc);
|
|
164
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#include <gtest/gtest.h>
|
|
2
|
+
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "Ecr17Protocol/Ecr17Protocol.hpp"
|
|
7
|
+
|
|
8
|
+
using margelo::nitro::ecr17::Ecr17Protocol;
|
|
9
|
+
|
|
10
|
+
// Field layout reference (1-based positions from the Nexi ECR17 spec, Payment
|
|
11
|
+
// request "P"):
|
|
12
|
+
// 1 8 Terminal ID
|
|
13
|
+
// 9 1 Reserved '0'
|
|
14
|
+
// 10 1 Message code 'P'
|
|
15
|
+
// 11 8 Cash register ID
|
|
16
|
+
// 19 1 Presence of additional data for GT
|
|
17
|
+
// 20 2 Reserved "00"
|
|
18
|
+
// 22 1 Start transaction when card already present
|
|
19
|
+
// 23 1 Payment type
|
|
20
|
+
// 24 8 Transaction amount (right aligned, '0' filled)
|
|
21
|
+
// 32 128 Text to print (' ' filled)
|
|
22
|
+
// 160 8 Reserved '0' => total 167 bytes
|
|
23
|
+
|
|
24
|
+
TEST(Protocol, StatusMessageLayout) {
|
|
25
|
+
std::string m = Ecr17Protocol::buildStatusMessage("42");
|
|
26
|
+
ASSERT_EQ(m.size(), 10u);
|
|
27
|
+
EXPECT_EQ(m.substr(0, 8), "00000042"); // terminal id, left-padded
|
|
28
|
+
EXPECT_EQ(m[8], '0'); // reserved
|
|
29
|
+
EXPECT_EQ(m[9], 's'); // lowercase message code per spec
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
TEST(Protocol, StatusMessageKeepsFullWidthId) {
|
|
33
|
+
std::string m = Ecr17Protocol::buildStatusMessage("12345678");
|
|
34
|
+
EXPECT_EQ(m, "123456780s");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
TEST(Protocol, PaymentMessageIs167Bytes) {
|
|
38
|
+
std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 650);
|
|
39
|
+
EXPECT_EQ(m.size(), 167u);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
TEST(Protocol, PaymentMessageFieldLayout) {
|
|
43
|
+
std::string m = Ecr17Protocol::buildPaymentMessage("12345678", "87654321", 650);
|
|
44
|
+
ASSERT_EQ(m.size(), 167u);
|
|
45
|
+
EXPECT_EQ(m.substr(0, 8), "12345678"); // terminal id
|
|
46
|
+
EXPECT_EQ(m[8], '0'); // reserved
|
|
47
|
+
EXPECT_EQ(m[9], 'P'); // message code
|
|
48
|
+
EXPECT_EQ(m.substr(10, 8), "87654321"); // cash register id
|
|
49
|
+
EXPECT_EQ(m[18], '0'); // presence of additional data
|
|
50
|
+
EXPECT_EQ(m.substr(19, 2), "00"); // reserved
|
|
51
|
+
EXPECT_EQ(m[21], '0'); // start-with-card
|
|
52
|
+
EXPECT_EQ(m[22], '0'); // payment type
|
|
53
|
+
EXPECT_EQ(m.substr(23, 8), "00000650"); // amount, right aligned
|
|
54
|
+
EXPECT_EQ(m.substr(31, 128), std::string(128, ' ')); // text field
|
|
55
|
+
EXPECT_EQ(m.substr(159, 8), "00000000"); // trailing reserved
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
TEST(Protocol, PaymentMessageAmountMaxFits) {
|
|
59
|
+
std::string m = Ecr17Protocol::buildPaymentMessage("1", "2", 99999999);
|
|
60
|
+
EXPECT_EQ(m.substr(23, 8), "99999999");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
TEST(Protocol, PaymentRejectsNegativeAmount) {
|
|
64
|
+
EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", -1), std::invalid_argument);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(Protocol, PaymentRejectsAmountOverflowingField) {
|
|
68
|
+
// 9 digits does not fit the 8-byte amount field.
|
|
69
|
+
EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("1", "2", 100000000), std::invalid_argument);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
TEST(Protocol, PaymentRejectsOversizedTerminalId) {
|
|
73
|
+
EXPECT_THROW(Ecr17Protocol::buildPaymentMessage("123456789", "2", 650), std::invalid_argument);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
TEST(Protocol, StatusRejectsOversizedTerminalId) {
|
|
77
|
+
EXPECT_THROW(Ecr17Protocol::buildStatusMessage("123456789"), std::invalid_argument);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Reversal request "S" layout (1-based spec positions):
|
|
81
|
+
// 1 8 Terminal ID · 9 1 Reserved · 10 1 'S' · 11 8 Cash register ID
|
|
82
|
+
// 19 6 STAN · 25 1 Presence of GT data · 26 1 Reserved => 26 bytes
|
|
83
|
+
TEST(Protocol, ReversalMessageLayout) {
|
|
84
|
+
std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321", "000123");
|
|
85
|
+
ASSERT_EQ(m.size(), 26u);
|
|
86
|
+
EXPECT_EQ(m.substr(0, 8), "12345678");
|
|
87
|
+
EXPECT_EQ(m[8], '0');
|
|
88
|
+
EXPECT_EQ(m[9], 'S');
|
|
89
|
+
EXPECT_EQ(m.substr(10, 8), "87654321");
|
|
90
|
+
EXPECT_EQ(m.substr(18, 6), "000123");
|
|
91
|
+
EXPECT_EQ(m[24], '0');
|
|
92
|
+
EXPECT_EQ(m[25], '0');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
TEST(Protocol, ReversalDefaultStanIsNoCheck) {
|
|
96
|
+
std::string m = Ecr17Protocol::buildReversalMessage("12345678", "87654321");
|
|
97
|
+
EXPECT_EQ(m.substr(18, 6), "000000");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
TEST(Protocol, ReversalRejectsOversizedStan) {
|
|
101
|
+
EXPECT_THROW(Ecr17Protocol::buildReversalMessage("1", "2", "1234567"), std::invalid_argument);
|
|
102
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Byte-layout tests for the full ECR17 command builder set (Phase 1).
|
|
2
|
+
// Positions are checked against the Nexi ECR17 spec tables in docs/.
|
|
3
|
+
|
|
4
|
+
#include <gtest/gtest.h>
|
|
5
|
+
|
|
6
|
+
#include <stdexcept>
|
|
7
|
+
#include <string>
|
|
8
|
+
|
|
9
|
+
#include "Ecr17Protocol/Ecr17Protocol.hpp"
|
|
10
|
+
|
|
11
|
+
using margelo::nitro::ecr17::Ecr17Protocol;
|
|
12
|
+
|
|
13
|
+
namespace {
|
|
14
|
+
constexpr char kFieldSep = 0x1B;
|
|
15
|
+
const std::string T = "12345678"; // terminal id
|
|
16
|
+
const std::string C = "87654321"; // cash register id
|
|
17
|
+
} // namespace
|
|
18
|
+
|
|
19
|
+
// --- Payment family (167 bytes) --------------------------------------------
|
|
20
|
+
|
|
21
|
+
TEST(Commands, ExtendedPaymentLayoutAndFlags) {
|
|
22
|
+
std::string m = Ecr17Protocol::buildExtendedPaymentMessage(T, C, 650, '2', true, true, "ABC");
|
|
23
|
+
ASSERT_EQ(m.size(), 167u);
|
|
24
|
+
EXPECT_EQ(m.substr(0, 8), T);
|
|
25
|
+
EXPECT_EQ(m[8], '0');
|
|
26
|
+
EXPECT_EQ(m[9], 'X');
|
|
27
|
+
EXPECT_EQ(m.substr(10, 8), C);
|
|
28
|
+
EXPECT_EQ(m[18], '1'); // withAdditionalData
|
|
29
|
+
EXPECT_EQ(m.substr(19, 2), "00");
|
|
30
|
+
EXPECT_EQ(m[21], '1'); // cardAlreadyPresent
|
|
31
|
+
EXPECT_EQ(m[22], '2'); // payment type
|
|
32
|
+
EXPECT_EQ(m.substr(23, 8), "00000650"); // amount
|
|
33
|
+
EXPECT_EQ(m.substr(31, 125), std::string(125, ' ')); // text left-padding
|
|
34
|
+
EXPECT_EQ(m.substr(156, 3), "ABC"); // text right-aligned
|
|
35
|
+
EXPECT_EQ(m.substr(159, 8), "00000000");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
TEST(Commands, PreAuthUsesCodeLowerP) {
|
|
39
|
+
std::string m = Ecr17Protocol::buildPreAuthMessage(T, C, 1000);
|
|
40
|
+
ASSERT_EQ(m.size(), 167u);
|
|
41
|
+
EXPECT_EQ(m[9], 'p');
|
|
42
|
+
EXPECT_EQ(m.substr(23, 8), "00001000");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
TEST(Commands, PaymentDefaultsMatchBasicLayout) {
|
|
46
|
+
std::string m = Ecr17Protocol::buildPaymentMessage(T, C, 650);
|
|
47
|
+
ASSERT_EQ(m.size(), 167u);
|
|
48
|
+
EXPECT_EQ(m[9], 'P');
|
|
49
|
+
EXPECT_EQ(m[18], '0'); // no additional data by default
|
|
50
|
+
EXPECT_EQ(m[21], '0'); // card not present by default
|
|
51
|
+
EXPECT_EQ(m[22], '0'); // auto payment type by default
|
|
52
|
+
EXPECT_EQ(m.substr(31, 128), std::string(128, ' '));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// --- Pre-auth integration / closure (176 bytes) ----------------------------
|
|
56
|
+
|
|
57
|
+
TEST(Commands, IncrementalLayout) {
|
|
58
|
+
std::string m = Ecr17Protocol::buildIncrementalMessage(T, C, 1000, "123456789");
|
|
59
|
+
ASSERT_EQ(m.size(), 176u);
|
|
60
|
+
EXPECT_EQ(m[9], 'i');
|
|
61
|
+
EXPECT_EQ(m.substr(19, 4), "0000");
|
|
62
|
+
EXPECT_EQ(m.substr(23, 8), "00001000");
|
|
63
|
+
EXPECT_EQ(m.substr(159, 9), "123456789"); // original pre-auth code
|
|
64
|
+
EXPECT_EQ(m.substr(168, 8), "00000000");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
TEST(Commands, PreAuthClosureLayout) {
|
|
68
|
+
std::string m = Ecr17Protocol::buildPreAuthClosureMessage(T, C, 500, "000000042");
|
|
69
|
+
ASSERT_EQ(m.size(), 176u);
|
|
70
|
+
EXPECT_EQ(m[9], 'c');
|
|
71
|
+
EXPECT_EQ(m.substr(159, 9), "000000042");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// --- Card verification (39 bytes) ------------------------------------------
|
|
75
|
+
|
|
76
|
+
TEST(Commands, CardVerificationLayout) {
|
|
77
|
+
std::string m = Ecr17Protocol::buildCardVerificationMessage(T, C, '1');
|
|
78
|
+
ASSERT_EQ(m.size(), 39u);
|
|
79
|
+
EXPECT_EQ(m[9], 'H');
|
|
80
|
+
EXPECT_EQ(m.substr(10, 8), C);
|
|
81
|
+
EXPECT_EQ(m[18], '0'); // no additional data
|
|
82
|
+
EXPECT_EQ(m.substr(19, 2), "00");
|
|
83
|
+
EXPECT_EQ(m[21], '0'); // standard verification
|
|
84
|
+
EXPECT_EQ(m[22], '1'); // payment type
|
|
85
|
+
EXPECT_EQ(m.substr(23, 16), std::string(16, '0'));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Session commands (26 bytes) -------------------------------------------
|
|
89
|
+
|
|
90
|
+
TEST(Commands, CloseSessionLayout) {
|
|
91
|
+
std::string m = Ecr17Protocol::buildCloseSessionMessage(T, C);
|
|
92
|
+
ASSERT_EQ(m.size(), 26u);
|
|
93
|
+
EXPECT_EQ(m[9], 'C');
|
|
94
|
+
EXPECT_EQ(m.substr(10, 8), C);
|
|
95
|
+
EXPECT_EQ(m[18], '0');
|
|
96
|
+
EXPECT_EQ(m.substr(19, 7), std::string(7, '0'));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
TEST(Commands, TotalsLayout) {
|
|
100
|
+
std::string m = Ecr17Protocol::buildTotalsMessage(T, C);
|
|
101
|
+
ASSERT_EQ(m.size(), 26u);
|
|
102
|
+
EXPECT_EQ(m[9], 'T');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- Send last result (22 bytes) -------------------------------------------
|
|
106
|
+
|
|
107
|
+
TEST(Commands, SendLastResultLayout) {
|
|
108
|
+
std::string m = Ecr17Protocol::buildSendLastResultMessage(T, C);
|
|
109
|
+
ASSERT_EQ(m.size(), 22u);
|
|
110
|
+
EXPECT_EQ(m[9], 'G');
|
|
111
|
+
EXPECT_EQ(m.substr(19, 3), "000");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Enable/disable ECR printing (11 bytes) --------------------------------
|
|
115
|
+
|
|
116
|
+
TEST(Commands, EnableEcrPrintLayout) {
|
|
117
|
+
EXPECT_EQ(Ecr17Protocol::buildEnableEcrPrintMessage(T, true), "123456780E1");
|
|
118
|
+
EXPECT_EQ(Ecr17Protocol::buildEnableEcrPrintMessage(T, false), "123456780E0");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- Reprint (22 bytes) -----------------------------------------------------
|
|
122
|
+
|
|
123
|
+
TEST(Commands, ReprintLayout) {
|
|
124
|
+
std::string m = Ecr17Protocol::buildReprintMessage(T, true);
|
|
125
|
+
ASSERT_EQ(m.size(), 22u);
|
|
126
|
+
EXPECT_EQ(m[9], 'R');
|
|
127
|
+
EXPECT_EQ(m[10], '1'); // send to ECR
|
|
128
|
+
EXPECT_EQ(m[11], '0'); // ticket type default
|
|
129
|
+
EXPECT_EQ(m.substr(12, 10), std::string(10, '0'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// --- VAS (variable, length-prefixed) ---------------------------------------
|
|
133
|
+
|
|
134
|
+
TEST(Commands, VasLayoutAndLengthPrefix) {
|
|
135
|
+
std::string m = Ecr17Protocol::buildVasMessage(T, C, "<x/>");
|
|
136
|
+
ASSERT_EQ(m.size(), 30u);
|
|
137
|
+
EXPECT_EQ(m[9], 'K');
|
|
138
|
+
EXPECT_EQ(m.substr(10, 8), C);
|
|
139
|
+
EXPECT_EQ(m.substr(18, 3), "000");
|
|
140
|
+
EXPECT_EQ(m[21], '0');
|
|
141
|
+
EXPECT_EQ(m.substr(22, 4), "0004"); // length of "<x/>"
|
|
142
|
+
EXPECT_EQ(m.substr(26), "<x/>");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
TEST(Commands, VasRejectsOversizedRequest) {
|
|
146
|
+
EXPECT_THROW(Ecr17Protocol::buildVasMessage(T, C, std::string(1025, 'x')), std::invalid_argument);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Additional data / tokenization 'U' ------------------------------------
|
|
150
|
+
|
|
151
|
+
TEST(Commands, AdditionalTagsLayout) {
|
|
152
|
+
const std::string content = "0COF0TRK123|0FNZ03"; // 18 chars
|
|
153
|
+
std::string m = Ecr17Protocol::buildAdditionalTagsMessage(T, content);
|
|
154
|
+
ASSERT_EQ(m.size(), 36u + content.size() + 1u);
|
|
155
|
+
EXPECT_EQ(m[9], 'U');
|
|
156
|
+
EXPECT_EQ(m.substr(10, 6), "000000");
|
|
157
|
+
EXPECT_EQ(m.substr(16, 2), "62");
|
|
158
|
+
EXPECT_EQ(m.substr(18, 8), "DF8D01 "); // left-justified, blank-filled
|
|
159
|
+
EXPECT_EQ(m[26], '0');
|
|
160
|
+
EXPECT_EQ(m.substr(27, 4), "0000");
|
|
161
|
+
EXPECT_EQ(m.substr(31, 5), "00000");
|
|
162
|
+
EXPECT_EQ(m.substr(36, content.size()), content);
|
|
163
|
+
EXPECT_EQ(m.back(), kFieldSep);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
TEST(Commands, AdditionalTagsRejectsBadContent) {
|
|
167
|
+
EXPECT_THROW(Ecr17Protocol::buildAdditionalTagsMessage(T, ""), std::invalid_argument);
|
|
168
|
+
EXPECT_THROW(Ecr17Protocol::buildAdditionalTagsMessage(T, std::string(101, 'x')),
|
|
169
|
+
std::invalid_argument);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
TEST(Commands, TokenizationTagFormat) {
|
|
173
|
+
EXPECT_EQ(Ecr17Protocol::formatTokenizationTag(false, "1666354841608"),
|
|
174
|
+
"0COF0TRK1666354841608|0FNZ03");
|
|
175
|
+
EXPECT_EQ(Ecr17Protocol::formatTokenizationTag(true, "ABC"), "0REC0TRKABC|0FNZ03");
|
|
176
|
+
EXPECT_THROW(Ecr17Protocol::formatTokenizationTag(false, ""), std::invalid_argument);
|
|
177
|
+
EXPECT_THROW(Ecr17Protocol::formatTokenizationTag(false, std::string(19, 'x')),
|
|
178
|
+
std::invalid_argument);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// --- Validation shared via leftPad -----------------------------------------
|
|
182
|
+
|
|
183
|
+
TEST(Commands, IncrementalRejectsOversizedPreAuthCode) {
|
|
184
|
+
EXPECT_THROW(Ecr17Protocol::buildIncrementalMessage(T, C, 100, "1234567890"),
|
|
185
|
+
std::invalid_argument); // 10 digits > 9-byte field
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
TEST(Commands, PreAuthRejectsNegativeAmount) {
|
|
189
|
+
EXPECT_THROW(Ecr17Protocol::buildPreAuthMessage(T, C, -1), std::invalid_argument);
|
|
190
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Tests for the ECR17 response parsers. Payloads are synthesized field-by-field
|
|
2
|
+
// at the exact 1-based offsets from the spec response tables in docs/. Helpers
|
|
3
|
+
// guarantee each field's width so offsets can't drift from a miscounted space.
|
|
4
|
+
|
|
5
|
+
#include <gtest/gtest.h>
|
|
6
|
+
|
|
7
|
+
#include <string>
|
|
8
|
+
|
|
9
|
+
#include "Ecr17Response/Ecr17Response.hpp"
|
|
10
|
+
|
|
11
|
+
using namespace margelo::nitro::ecr17;
|
|
12
|
+
|
|
13
|
+
namespace {
|
|
14
|
+
|
|
15
|
+
// Left-justified field, right-padded with spaces to `width` (alpha fields).
|
|
16
|
+
std::string a(const std::string& value, size_t width) {
|
|
17
|
+
std::string s = value;
|
|
18
|
+
s.resize(width, ' ');
|
|
19
|
+
return s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Right-justified numeric field, left-padded with '0' to `width`.
|
|
23
|
+
std::string n(const std::string& value, size_t width) {
|
|
24
|
+
return std::string(width - value.size(), '0') + value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
} // namespace
|
|
28
|
+
|
|
29
|
+
TEST(Response, PaymentPositive) {
|
|
30
|
+
std::string p = a("12345678", 8) + "0" + "E" + "00" + // header + result
|
|
31
|
+
n("4111111111", 19) + // PAN(19)
|
|
32
|
+
a("ICC", 3) + a("ABC123", 6) + "2111520" + // txType authCode dateTime
|
|
33
|
+
"2" + // cardType
|
|
34
|
+
a("ACQ", 11) + n("42", 6) + n("99", 6); // acquirer STAN idOnline
|
|
35
|
+
PaymentResponse r = Ecr17Response::parsePayment(p);
|
|
36
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
37
|
+
EXPECT_EQ(r.resultCode, "00");
|
|
38
|
+
EXPECT_EQ(r.pan, n("4111111111", 19));
|
|
39
|
+
EXPECT_EQ(r.transactionType, "ICC");
|
|
40
|
+
EXPECT_EQ(r.authCode, "ABC123");
|
|
41
|
+
EXPECT_EQ(r.hostDateTime, "2111520");
|
|
42
|
+
EXPECT_EQ(r.cardType, "2");
|
|
43
|
+
EXPECT_EQ(r.acquirerId, "ACQ");
|
|
44
|
+
EXPECT_EQ(r.stan, "000042");
|
|
45
|
+
EXPECT_EQ(r.onlineId, "000099");
|
|
46
|
+
EXPECT_FALSE(r.currency.applied);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
TEST(Response, PaymentNegative) {
|
|
50
|
+
std::string p = a("12345678", 8) + "0" + "E" + "01" + a("CARTA RIFIUTATA", 24) +
|
|
51
|
+
n("", 11) + // reserved 37-47
|
|
52
|
+
"3" + a("AC2", 11) + n("7", 6) + n("3", 6);
|
|
53
|
+
PaymentResponse r = Ecr17Response::parsePayment(p);
|
|
54
|
+
EXPECT_EQ(r.outcome, Outcome::Ko);
|
|
55
|
+
EXPECT_EQ(r.resultCode, "01");
|
|
56
|
+
EXPECT_EQ(r.errorDescription, "CARTA RIFIUTATA");
|
|
57
|
+
EXPECT_EQ(r.cardType, "3");
|
|
58
|
+
EXPECT_EQ(r.stan, "000007");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
TEST(Response, PaymentWithCurrencyExchange) {
|
|
62
|
+
std::string base = a("12345678", 8) + "0" + "V" + "00" + n("4111111111", 19) + a("ICC", 3) +
|
|
63
|
+
a("ABC123", 6) + "2111520" + "2" + a("ACQ", 11) + n("42", 6) + n("99", 6);
|
|
64
|
+
// actionCode(3) origAmount(8) flag(1) rate(8) ccy(3) amount(12) precision(1)
|
|
65
|
+
std::string p = base + "000" + n("650", 8) + "1" + n("12345", 8) + "USD" + n("650", 12) + "2";
|
|
66
|
+
PaymentResponse r = Ecr17Response::parsePayment(p);
|
|
67
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
68
|
+
EXPECT_TRUE(r.currency.applied);
|
|
69
|
+
EXPECT_EQ(r.currency.rate, "00012345");
|
|
70
|
+
EXPECT_EQ(r.currency.currencyCode, "USD");
|
|
71
|
+
EXPECT_EQ(r.currency.amount, "000000000650");
|
|
72
|
+
EXPECT_EQ(r.currency.precision, "2");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
TEST(Response, Status) {
|
|
76
|
+
std::string p = a("12345678", 8) + "0" + "s" + n("", 10) + // reserved 11-20
|
|
77
|
+
"0102251530" + "2" + "V1.2.3"; // dateTime status sw
|
|
78
|
+
StatusResponse r = Ecr17Response::parseStatus(p);
|
|
79
|
+
EXPECT_EQ(r.terminalId, "12345678");
|
|
80
|
+
EXPECT_EQ(r.dateTimeRaw, "0102251530");
|
|
81
|
+
EXPECT_EQ(r.status, 2);
|
|
82
|
+
EXPECT_EQ(r.softwareRelease, "V1.2.3");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
TEST(Response, Totals) {
|
|
86
|
+
std::string p = a("12345678", 8) + "0" + "T" + "00" + n("123456", 16) + n("", 6);
|
|
87
|
+
TotalsResponse r = Ecr17Response::parseTotals(p);
|
|
88
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
89
|
+
EXPECT_EQ(r.posTotal, n("123456", 16));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
TEST(Response, ClosePositive) {
|
|
93
|
+
std::string p = a("12345678", 8) + "0" + "C" + "00" + n("1000", 16) + n("1000", 16);
|
|
94
|
+
CloseResponse r = Ecr17Response::parseClose(p);
|
|
95
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
96
|
+
EXPECT_EQ(r.posTotal, n("1000", 16));
|
|
97
|
+
EXPECT_EQ(r.hostTotal, n("1000", 16));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
TEST(Response, CloseNegative) {
|
|
101
|
+
std::string p = a("12345678", 8) + "0" + "C" + "01" + a("SBILANCIO", 19) + "100";
|
|
102
|
+
CloseResponse r = Ecr17Response::parseClose(p);
|
|
103
|
+
EXPECT_EQ(r.outcome, Outcome::Ko);
|
|
104
|
+
EXPECT_EQ(r.errorDescription, "SBILANCIO");
|
|
105
|
+
EXPECT_EQ(r.actionCode, "100");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
TEST(Response, PreAuthPositive) {
|
|
109
|
+
std::string p = a("12345678", 8) + "0" + "e" + "00" + n("4111111111", 19) + a("CLI", 3) +
|
|
110
|
+
a("AUTH01", 6) + n("50000", 8) + n("123", 9) + "000" + "2111520";
|
|
111
|
+
PreAuthResponse r = Ecr17Response::parsePreAuth(p);
|
|
112
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
113
|
+
EXPECT_EQ(r.transactionType, "CLI");
|
|
114
|
+
EXPECT_EQ(r.authCode, "AUTH01");
|
|
115
|
+
EXPECT_EQ(r.preAuthorizedAmount, "00050000");
|
|
116
|
+
EXPECT_EQ(r.preAuthCode, "000000123");
|
|
117
|
+
EXPECT_EQ(r.hostDateTime, "2111520");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Regression: on an approved pre-auth the amount field occupies positions 41-48,
|
|
121
|
+
// so its last digit sits exactly where cardType would be read. An amount ending
|
|
122
|
+
// in 1/2/3 must NOT be surfaced as debit/credit/other. cardType is only
|
|
123
|
+
// meaningful for the KO layout.
|
|
124
|
+
TEST(Response, PreAuthPositiveDoesNotLeakAmountDigitAsCardType) {
|
|
125
|
+
std::string p = a("12345678", 8) + "0" + "e" + "00" + n("4111111111", 19) + a("CLI", 3) +
|
|
126
|
+
a("AUTH01", 6) + n("50001", 8) + n("123", 9) + "000" + "2111520";
|
|
127
|
+
PreAuthResponse r = Ecr17Response::parsePreAuth(p);
|
|
128
|
+
EXPECT_EQ(r.outcome, Outcome::Ok);
|
|
129
|
+
EXPECT_EQ(r.preAuthorizedAmount, "00050001"); // ends in '1'
|
|
130
|
+
EXPECT_EQ(r.cardType, ""); // must stay empty, not "1"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
TEST(Response, Vas) {
|
|
134
|
+
std::string xml =
|
|
135
|
+
"<ecrres><p k=\"RESPID\">0</p><p k=\"RESPMSG\">OK-APPROVED</p>"
|
|
136
|
+
"<p k=\"ORDER_ID\">ABC123</p></ecrres>";
|
|
137
|
+
// header(10) reserved(4) concatFlag(1) idMessage(3) filler-to-pos27(8) xml
|
|
138
|
+
std::string p = a("12345678", 8) + "0" + "K" + n("", 4) + "0" + "001" + n("", 8) + xml;
|
|
139
|
+
VasResponse r = Ecr17Response::parseVas(p);
|
|
140
|
+
EXPECT_FALSE(r.moreMessages);
|
|
141
|
+
EXPECT_EQ(r.idMessage, "001");
|
|
142
|
+
EXPECT_EQ(r.responseId, "0");
|
|
143
|
+
EXPECT_EQ(r.responseMessage, "OK-APPROVED");
|
|
144
|
+
EXPECT_EQ(r.orderId, "ABC123");
|
|
145
|
+
EXPECT_EQ(r.rawXml, xml);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
TEST(Response, DefensiveOnShortOrEmptyPayload) {
|
|
149
|
+
PaymentResponse r = Ecr17Response::parsePayment("");
|
|
150
|
+
EXPECT_EQ(r.outcome, Outcome::Unknown);
|
|
151
|
+
EXPECT_EQ(r.resultCode, "");
|
|
152
|
+
EXPECT_EQ(r.pan, "");
|
|
153
|
+
|
|
154
|
+
StatusResponse s = Ecr17Response::parseStatus("123"); // truncated, must not crash
|
|
155
|
+
EXPECT_EQ(s.status, -1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
TEST(Response, OutcomeMapping) {
|
|
159
|
+
EXPECT_EQ(outcomeFromCode("00"), Outcome::Ok);
|
|
160
|
+
EXPECT_EQ(outcomeFromCode("01"), Outcome::Ko);
|
|
161
|
+
EXPECT_EQ(outcomeFromCode("05"), Outcome::CardNotPresent);
|
|
162
|
+
EXPECT_EQ(outcomeFromCode("09"), Outcome::UnknownTag);
|
|
163
|
+
EXPECT_EQ(outcomeFromCode("zz"), Outcome::Unknown);
|
|
164
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Money-critical safety tests for the auto-reconnect retry decision.
|
|
2
|
+
// The terminal handles real payments, so a financial command must NEVER be
|
|
3
|
+
// blindly re-sent after a connection drop (double-charge risk).
|
|
4
|
+
|
|
5
|
+
#include <gtest/gtest.h>
|
|
6
|
+
|
|
7
|
+
#include "Session/RetryPolicy.hpp"
|
|
8
|
+
|
|
9
|
+
using margelo::nitro::ecr17::shouldRetryAfterReconnect;
|
|
10
|
+
|
|
11
|
+
// A financial command (safeToRetry == false) must never be retried, regardless
|
|
12
|
+
// of autoReconnect / drop state. This is the invariant that prevents double
|
|
13
|
+
// charging; recovery is via sendLastResult ('G'), not a re-send.
|
|
14
|
+
TEST(RetryPolicy, FinancialCommandIsNeverRetried) {
|
|
15
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/true, /*safe=*/false));
|
|
16
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/true, /*safe=*/false));
|
|
17
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/false, /*safe=*/false));
|
|
18
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/false, /*safe=*/false));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A safe/idempotent command is retried ONLY when autoReconnect is on AND the
|
|
22
|
+
// transport actually dropped.
|
|
23
|
+
TEST(RetryPolicy, SafeCommandRetriedOnlyOnReconnectAfterDrop) {
|
|
24
|
+
EXPECT_TRUE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/true, /*safe=*/true));
|
|
25
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/true, /*safe=*/true));
|
|
26
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/true, /*dropped=*/false, /*safe=*/true));
|
|
27
|
+
EXPECT_FALSE(shouldRetryAfterReconnect(/*autoReconnect=*/false, /*dropped=*/false, /*safe=*/true));
|
|
28
|
+
}
|