rewrite for eet 2.0 api version v4

This commit is contained in:
2026-10-13 23:28:55 +02:00
parent c926a173ba
commit 3f8e4796b3
55 changed files with 3698 additions and 1611 deletions
+145
View File
@@ -0,0 +1,145 @@
find_package(Qt6 6.5 REQUIRED COMPONENTS Test)
add_executable(EetCppV4RequestTest
eetv4request_test.cpp
)
set_target_properties(EetCppV4RequestTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4RequestTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.request COMMAND EetCppV4RequestTest)
add_executable(EetCppV4XmlTest
eetv4xml_test.cpp
)
set_target_properties(EetCppV4XmlTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4XmlTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.xml COMMAND EetCppV4XmlTest)
add_executable(EetCppV4SoapTest
eetv4soap_test.cpp
)
set_target_properties(EetCppV4SoapTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4SoapTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.soap COMMAND EetCppV4SoapTest)
add_executable(EetCppV4SignerTest
eetv4signer_test.cpp
)
set_target_properties(EetCppV4SignerTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4SignerTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.signer COMMAND EetCppV4SignerTest)
add_executable(EetCppV4ResponseTest
eetv4response_test.cpp
)
set_target_properties(EetCppV4ResponseTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ResponseTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.response COMMAND EetCppV4ResponseTest)
add_executable(EetCppV4ClientTest
eetv4client_test.cpp
)
set_target_properties(EetCppV4ClientTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ClientTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.client COMMAND EetCppV4ClientTest)
add_executable(EetCppV4ClientIntegrationTest
eetv4client_integration_test.cpp
)
set_target_properties(EetCppV4ClientIntegrationTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCppV4ClientIntegrationTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.v4.client.integration COMMAND EetCppV4ClientIntegrationTest)
add_executable(EetCaTest
eetca_test.cpp
)
set_target_properties(EetCaTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCaTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.ca COMMAND EetCaTest)
add_executable(EetCaIntegrationTest
eetca_integration_test.cpp
)
set_target_properties(EetCaIntegrationTest PROPERTIES
AUTOMOC ON
)
target_link_libraries(EetCaIntegrationTest
PRIVATE
EetCpp::EetCpp
Qt6::Test
)
add_test(NAME EetCpp.ca.integration COMMAND EetCaIntegrationTest)
+150
View File
@@ -0,0 +1,150 @@
#include "eetca.h"
#include <QFile>
#include <QProcess>
#include <QRegularExpression>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QTemporaryDir>
#include <QtTest>
namespace
{
class MockCaServer : public QSslServer
{
public:
explicit MockCaServer(const QByteArray &pkcs12)
: m_pkcs12(pkcs12)
{
connect(this, &QSslServer::pendingConnectionAvailable, this, [this] {
QSslSocket *socket = qobject_cast<QSslSocket *>(nextPendingConnection());
if (socket == nullptr)
{
return;
}
connect(socket, &QSslSocket::readyRead, this, [this, socket] {
const QByteArray request = socket->readAll();
const QByteArray firstLine = request.left(request.indexOf("\r\n"));
const QList<QByteArray> parts = firstLine.split(' ');
if (parts.size() < 2 || !request.contains("Authorization: Bearer "))
{
socket->disconnectFromHost();
return;
}
QByteArray body;
int status = 200;
if (parts.at(1) == "/api/request/renew")
{
body = "{\"reqId\":\"123e4567-e89b-42d3-a456-426614174000\"}";
status = 201;
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/status")
{
body = "{\"reqId\":\"123e4567-e89b-42d3-a456-426614174000\","
"\"state\":\"ISSUED\"}";
}
else if (parts.at(1) == "/api/request/not-finished")
{
body = "{\"count\":1,\"data\":[\"123e4567-e89b-42d3-a456-426614174000\"]}";
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/claim-download")
{
body = "{\"pkcs12\":\"" + m_pkcs12.toBase64() +
"\",\"password\":\"ABcd1234\",\"certificate\":{"
"\"sn\":\"12345\",\"validFrom\":\"2026-01-01T00:00:00+00:00\","
"\"validTo\":\"2027-01-01T00:00:00+00:00\",\"state\":\"VALID\"}}";
}
else if (parts.at(1) == "/api/request/123e4567-e89b-42d3-a456-426614174000/ack-download")
{
status = 204;
}
else
{
status = 404;
body = "{\"code\":4,\"message\":\"Unknown object.\"}";
}
const QByteArray response =
"HTTP/1.1 " + QByteArray::number(status) + " OK\r\n"
"Content-Type: application/json\r\nContent-Length: " +
QByteArray::number(body.size()) + "\r\nConnection: close\r\n\r\n" + body;
socket->write(response);
socket->disconnectFromHost();
});
});
}
private:
QByteArray m_pkcs12;
};
}
class EetCaIntegrationTest : public QObject
{
Q_OBJECT
private slots:
void runsCertificateWorkflow();
};
void EetCaIntegrationTest::runsCertificateWorkflow()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certPath = directory.filePath(QStringLiteral("certificate.pem"));
const QString p12Path = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=localhost"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certPath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
p12Path, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"), certPath,
QStringLiteral("-passout"), QStringLiteral("pass:test")});
QFile p12File(p12Path);
QVERIFY(p12File.open(QIODevice::ReadOnly));
const QByteArray p12 = p12File.readAll();
MockCaServer server(p12);
QSslConfiguration ssl = QSslConfiguration::defaultConfiguration();
QFile keyFile(keyPath);
QFile certificateFile(certPath);
QVERIFY(keyFile.open(QIODevice::ReadOnly));
QVERIFY(certificateFile.open(QIODevice::ReadOnly));
ssl.setPrivateKey(QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem));
ssl.setLocalCertificate(QSslCertificate(certificateFile.readAll(), QSsl::Pem));
server.setSslConfiguration(ssl);
QVERIFY(server.listen(QHostAddress::LocalHost));
QSslConfiguration clientSsl = QSslConfiguration::defaultConfiguration();
clientSsl.setCaCertificates({ssl.localCertificate()});
EetCaClient client({
QUrl(QStringLiteral("https://localhost:%1/api").arg(server.serverPort())),
p12Path, QByteArrayLiteral("test"), 5000, clientSsl
});
QString error;
QUuid requestId;
QVERIFY2(client.requestRenewal(&requestId, &error), qPrintable(error));
QCOMPARE(requestId, QUuid(QStringLiteral("123e4567-e89b-42d3-a456-426614174000")));
EetCaRequestStatus status;
QVERIFY2(client.requestStatus(requestId, &status, &error), qPrintable(error));
QCOMPARE(status.state, QStringLiteral("ISSUED"));
QList<QUuid> unfinished;
QVERIFY2(client.listUnfinished(&unfinished, &error), qPrintable(error));
QCOMPARE(unfinished, QList<QUuid>{requestId});
EetCaPkcs12 package;
QVERIFY2(client.claimDownload(requestId, &package, &error), qPrintable(error));
QCOMPARE(package.pkcs12, p12);
QCOMPARE(package.password, QByteArrayLiteral("ABcd1234"));
QVERIFY2(client.acknowledgeDownload(requestId, &error), qPrintable(error));
}
QTEST_MAIN(EetCaIntegrationTest)
#include "eetca_integration_test.moc"
+35
View File
@@ -0,0 +1,35 @@
#include "eetca.h"
#include <QtTest>
class EetCaTest : public QObject
{
Q_OBJECT
private slots:
void rejectsInvalidConfiguration();
void rejectsMissingSigningCertificate();
};
void EetCaTest::rejectsInvalidConfiguration()
{
EetCaClient client({QUrl(QStringLiteral("http://localhost/api")),
QStringLiteral("missing.p12"), QByteArrayLiteral("password"), 1000});
QUuid requestId;
QString error;
QVERIFY(!client.requestRenewal(&requestId, &error));
QCOMPARE(error, QStringLiteral("CA API requires a valid HTTPS base URL and positive timeout."));
}
void EetCaTest::rejectsMissingSigningCertificate()
{
EetCaClient client({QUrl(QStringLiteral("https://localhost/api")),
QStringLiteral("missing.p12"), QByteArrayLiteral("password"), 1000});
QUuid requestId;
QString error;
QVERIFY(!client.requestRenewal(&requestId, &error));
QCOMPARE(error, QStringLiteral("Cannot read the CA signing PKCS#12 file."));
}
QTEST_MAIN(EetCaTest)
#include "eetca_test.moc"
+139
View File
@@ -0,0 +1,139 @@
#include "eetv4client.h"
#include "eetv4signer.h"
#include <QFile>
#include <QProcess>
#include <QSslCertificate>
#include <QSslKey>
#include <QSslServer>
#include <QTemporaryDir>
#include <QtTest>
namespace
{
class MockEetServer : public QSslServer
{
public:
explicit MockEetServer(const QByteArray &response)
: m_response(response)
{
connect(this, &QSslServer::pendingConnectionAvailable, this, [this] {
QSslSocket *socket = qobject_cast<QSslSocket *>(nextPendingConnection());
if (socket == nullptr)
{
return;
}
connect(socket, &QSslSocket::readyRead, this, [this, socket] {
const QByteArray request = socket->readAll();
const qsizetype bodyStart = request.indexOf("\r\n\r\n");
const QByteArray soap = bodyStart < 0 ? QByteArray()
: request.mid(bodyStart + 4);
QString verificationError;
m_requestVerified = !soap.isEmpty() &&
EetV4Signer::verifySoap(soap, &verificationError);
const QByteArray http =
"HTTP/1.1 200 OK\r\nContent-Type: text/xml; charset=utf-8\r\n"
"Content-Length: " + QByteArray::number(m_response.size()) +
"\r\nConnection: close\r\n\r\n" + m_response;
socket->write(http);
socket->disconnectFromHost();
});
});
}
bool requestVerified() const
{
return m_requestVerified;
}
private:
QByteArray m_response;
bool m_requestVerified{false};
};
}
class EetV4ClientIntegrationTest : public QObject
{
Q_OBJECT
private slots:
void sendsSignedSaleAndParsesPok();
};
void EetV4ClientIntegrationTest::sendsSignedSaleAndParsesPok()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certPath = directory.filePath(QStringLiteral("certificate.pem"));
const QString p12Path = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=localhost"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certPath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
p12Path, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"), certPath,
QStringLiteral("-passout"), QStringLiteral("pass:test")});
const QByteArray unsignedResponse =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-secext-1.0.xsd\""
" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/"
"oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
"<soap:Header/><soap:Body wsu:Id=\"response-body\">"
"<Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka uuid_zpravy=\"123e4567-e89b-42d3-a456-426614174000\" "
"dat_prij=\"2026-10-13T20:00:00+02:00\"/>"
"<Potvrzeni pok=\"123e4567-e89b-42d3-a456-426614174000-01\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
QString signingError;
const QByteArray signedResponse = EetV4Signer::signSoap(
unsignedResponse, p12Path, QByteArrayLiteral("test"), &signingError);
QVERIFY2(!signedResponse.isEmpty(), qPrintable(signingError));
MockEetServer server(signedResponse);
QSslConfiguration serverSsl = QSslConfiguration::defaultConfiguration();
QFile keyFile(keyPath);
QFile certificateFile(certPath);
QVERIFY(keyFile.open(QIODevice::ReadOnly));
QVERIFY(certificateFile.open(QIODevice::ReadOnly));
serverSsl.setPrivateKey(QSslKey(keyFile.readAll(), QSsl::Rsa, QSsl::Pem));
serverSsl.setLocalCertificate(QSslCertificate(certificateFile.readAll(), QSsl::Pem));
server.setSslConfiguration(serverSsl);
QVERIFY(server.listen(QHostAddress::LocalHost));
QSslConfiguration clientSsl = QSslConfiguration::defaultConfiguration();
clientSsl.setCaCertificates({serverSsl.localCertificate()});
EetV4Client client({
QUrl(QStringLiteral("https://localhost:%1/eet").arg(server.serverPort())),
5000, clientSsl
});
EetV4SaleRequest request;
request.sentAt = QDateTime::currentDateTime().toOffsetFromUtc(0);
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ123456789");
request.unitId = 1;
request.cashRegisterId = QStringLiteral("KASA1");
request.receiptNumber = QStringLiteral("TEST-1");
QVERIFY(EetV4Amount::fromString(QStringLiteral("100.00"), &request.totalSale));
EetV4Response response;
QString error;
QVERIFY2(client.sendSale(request, p12Path, QByteArrayLiteral("test"), &response, &error),
qPrintable(error));
QVERIFY(server.requestVerified());
QVERIFY(response.isSuccess());
QCOMPARE(response.pok, QStringLiteral("123e4567-e89b-42d3-a456-426614174000-01"));
}
QTEST_MAIN(EetV4ClientIntegrationTest)
#include "eetv4client_integration_test.moc"
+33
View File
@@ -0,0 +1,33 @@
#include "eetv4client.h"
#include <QtTest>
class EetV4ClientTest : public QObject
{
Q_OBJECT
private slots:
void rejectsNonHttpsEndpoint();
void rejectsNonPositiveTimeout();
};
void EetV4ClientTest::rejectsNonHttpsEndpoint()
{
EetV4Client client({QUrl(QStringLiteral("http://localhost/eet")), 1000});
EetV4Response response;
QString error;
QVERIFY(!client.sendSale({}, QStringLiteral("missing.p12"), {}, &response, &error));
QCOMPARE(error, QStringLiteral("EET endpoint must be a valid HTTPS URL."));
}
void EetV4ClientTest::rejectsNonPositiveTimeout()
{
EetV4Client client({QUrl(QStringLiteral("https://localhost/eet")), 0});
EetV4Response response;
QString error;
QVERIFY(!client.sendSale({}, QStringLiteral("missing.p12"), {}, &response, &error));
QCOMPARE(error, QStringLiteral("EET timeout must be positive."));
}
QTEST_MAIN(EetV4ClientTest)
#include "eetv4client_test.moc"
+64
View File
@@ -0,0 +1,64 @@
#include <QTest>
#include "../libEet/eetv4request.h"
class EetV4RequestTest : public QObject
{
Q_OBJECT
private slots:
void parsesFixedPointAmounts()
{
EetV4Amount amount;
QString error;
QVERIFY(EetV4Amount::fromString(QStringLiteral("123.45"), &amount, &error));
QCOMPARE(amount.cents(), qint64(12345));
QCOMPARE(amount.toString(), QStringLiteral("123.45"));
QVERIFY(EetV4Amount::fromString(QStringLiteral("-0.01"), &amount, &error));
QCOMPARE(amount.cents(), qint64(-1));
QCOMPARE(amount.toString(), QStringLiteral("-0.01"));
}
void rejectsInvalidFixedPointAmounts()
{
EetV4Amount amount;
QString error;
QVERIFY(!EetV4Amount::fromString(QStringLiteral("1"), &amount, &error));
QVERIFY(!EetV4Amount::fromString(QStringLiteral("100000000.00"), &amount, &error));
QVERIFY(!EetV4Amount::fromString(QStringLiteral("-0.00"), &amount, &error));
}
void validatesSaleRequest()
{
EetV4SaleRequest request;
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
QStringList errors;
QVERIFY(request.validate(&errors));
QVERIFY(errors.isEmpty());
request.taxpayerEic = QStringLiteral("CZ12");
QVERIFY(!request.validate(&errors));
QVERIFY(!errors.isEmpty());
}
void formatsOffsetDateTime()
{
const QDateTime dateTime(QDate(2027, 1, 1), QTime(12, 34, 56),
QTimeZone::fromSecondsAheadOfUtc(-18000));
QCOMPARE(EetV4SaleRequest::formatDateTime(dateTime),
QStringLiteral("2027-01-01T12:34:56-05:00"));
}
};
QTEST_MAIN(EetV4RequestTest)
#include "eetv4request_test.moc"
+9
View File
@@ -0,0 +1,9 @@
QT += core testlib
CONFIG += c++11 console testcase
TEMPLATE = app
INCLUDEPATH += ../libEet
SOURCES += \
eetv4request_test.cpp \
../libEet/eetv4request.cpp
+80
View File
@@ -0,0 +1,80 @@
#include "eetv4response.h"
#include <QtTest>
class EetV4ResponseTest : public QObject
{
Q_OBJECT
private slots:
void parsesConfirmation();
void parsesError();
void parsesSoapFault();
void rejectsInvalidResponse();
};
void EetV4ResponseTest::parsesConfirmation()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka uuid_zpravy=\"123e4567-e89b-42d3-a456-426614174000\" "
"dat_prij=\"2026-10-13T20:00:00+02:00\"/>"
"<Potvrzeni pok=\"123e4567-e89b-42d3-a456-426614174000-01\" test=\"true\"/>"
"<Varovani kod_varov=\"12\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QString error;
QVERIFY2(EetV4ResponseParser::parseSoap(soap, &response, &error), qPrintable(error));
QCOMPARE(response.status, EetV4ResponseStatus::Success);
QCOMPARE(response.pok, QStringLiteral("123e4567-e89b-42d3-a456-426614174000-01"));
QCOMPARE(response.messageUuid, QStringLiteral("123e4567-e89b-42d3-a456-426614174000"));
QVERIFY(response.test);
QCOMPARE(response.warningCodes, QList<int>{12});
QVERIFY(response.receivedAt.isValid());
}
void EetV4ResponseTest::parsesError()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka dat_odmit=\"2026-10-13T20:00:00Z\"/>"
"<Chyba kod=\"-25\">Invalid certificate</Chyba>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QVERIFY(EetV4ResponseParser::parseSoap(soap, &response));
QCOMPARE(response.status, EetV4ResponseStatus::Error);
QCOMPARE(response.errorCode, -25);
QCOMPARE(response.message, QStringLiteral("Invalid certificate"));
QVERIFY(response.rejectedAt.isValid());
}
void EetV4ResponseTest::parsesSoapFault()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><soap:Fault><faultcode>soap:Server</faultcode>"
"<faultstring>Service unavailable</faultstring></soap:Fault>"
"</soap:Body></soap:Envelope>";
EetV4Response response;
QVERIFY(EetV4ResponseParser::parseSoap(soap, &response));
QCOMPARE(response.status, EetV4ResponseStatus::SoapFault);
QCOMPARE(response.message, QStringLiteral("soap:Server: Service unavailable"));
}
void EetV4ResponseTest::rejectsInvalidResponse()
{
const QByteArray soap =
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">"
"<soap:Body><Odpoved xmlns=\"http://fs.gov.cz/eet/schema/v4\">"
"<Hlavicka/><Potvrzeni pok=\"not-a-pok\"/>"
"</Odpoved></soap:Body></soap:Envelope>";
EetV4Response response;
QString error;
QVERIFY(!EetV4ResponseParser::parseSoap(soap, &response, &error));
QVERIFY(!error.isEmpty());
}
QTEST_MAIN(EetV4ResponseTest)
#include "eetv4response_test.moc"
+62
View File
@@ -0,0 +1,62 @@
#include "eetv4signer.h"
#include <QProcess>
#include <QTemporaryDir>
#include <QtTest>
class EetV4SignerTest : public QObject
{
Q_OBJECT
private slots:
void signsSoapWithPkcs12();
};
void EetV4SignerTest::signsSoapWithPkcs12()
{
QTemporaryDir directory;
QVERIFY(directory.isValid());
const QString keyPath = directory.filePath(QStringLiteral("key.pem"));
const QString certificatePath = directory.filePath(QStringLiteral("certificate.pem"));
const QString bundlePath = directory.filePath(QStringLiteral("certificate.p12"));
auto runOpenSsl = [](const QStringList &arguments) {
QProcess process;
process.start(QStringLiteral("openssl"), arguments);
QVERIFY2(process.waitForFinished(30000), qPrintable(process.errorString()));
QVERIFY2(process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0,
process.readAllStandardError().constData());
};
runOpenSsl({QStringLiteral("req"), QStringLiteral("-x509"), QStringLiteral("-newkey"),
QStringLiteral("rsa:2048"), QStringLiteral("-nodes"), QStringLiteral("-subj"),
QStringLiteral("/CN=EetCppTest"), QStringLiteral("-keyout"), keyPath,
QStringLiteral("-out"), certificatePath, QStringLiteral("-days"), QStringLiteral("1")});
runOpenSsl({QStringLiteral("pkcs12"), QStringLiteral("-export"), QStringLiteral("-out"),
bundlePath, QStringLiteral("-inkey"), keyPath, QStringLiteral("-in"),
certificatePath, QStringLiteral("-passout"), QStringLiteral("pass:test")});
const QByteArray unsignedSoap =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
" xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\""
" xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">"
"<soap:Header/><soap:Body wsu:Id=\"body-id\"><test/></soap:Body></soap:Envelope>";
QString error;
const QByteArray signedSoap = EetV4Signer::signSoap(
unsignedSoap, bundlePath, QByteArrayLiteral("test"), &error);
QVERIFY2(!signedSoap.isEmpty(), qPrintable(error));
QVERIFY(signedSoap.contains("<ds:Signature"));
QVERIFY(signedSoap.contains("URI=\"#body-id\""));
QVERIFY(signedSoap.contains("BinarySecurityToken"));
QString verificationError;
QVERIFY2(EetV4Signer::verifySoap(signedSoap, &verificationError),
qPrintable(verificationError));
QByteArray tamperedSoap = signedSoap;
tamperedSoap.replace("<test/>", "<test>changed</test>");
QVERIFY(!EetV4Signer::verifySoap(tamperedSoap, &verificationError));
}
QTEST_MAIN(EetV4SignerTest)
#include "eetv4signer_test.moc"
+67
View File
@@ -0,0 +1,67 @@
#include <QTest>
#include <QXmlStreamReader>
#include "../libEet/eetv4soap.h"
class EetV4SoapTest : public QObject
{
Q_OBJECT
private slots:
void serializesSoapEnvelope()
{
EetV4SaleRequest request;
request.messageUuid = QUuid(QStringLiteral("550e8400-e29b-41d4-a716-446655440000"));
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("123.45"), &request.totalSale);
QString error;
const QByteArray soap = EetV4Soap::serializeUnsigned(request, QByteArrayLiteral("body-1"), &error);
QVERIFY2(!soap.isEmpty(), qPrintable(error));
QVERIFY(soap.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
QVERIFY(soap.contains("<soap:Envelope"));
QVERIFY(soap.contains("<soap:Header/>"));
QVERIFY(soap.contains("<soap:Body wsu:Id=\"body-1\">"));
QVERIFY(soap.contains("<Trzba xmlns=\"http://fs.gov.cz/eet/schema/v4\">"));
QVERIFY(soap.endsWith("</soap:Body></soap:Envelope>"));
}
void rejectsInvalidBodyId()
{
EetV4SaleRequest request;
QString error;
QVERIFY(EetV4Soap::serializeUnsigned(request, QByteArrayLiteral("invalid id"), &error).isEmpty());
QVERIFY(!error.isEmpty());
}
void producesParseableSoap()
{
EetV4SaleRequest request;
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("1.00"), &request.totalSale);
QXmlStreamReader reader(EetV4Soap::serializeUnsigned(request));
while (!reader.atEnd())
{
reader.readNext();
}
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
}
};
QTEST_MAIN(EetV4SoapTest)
#include "eetv4soap_test.moc"
+87
View File
@@ -0,0 +1,87 @@
#include <QTest>
#include <QXmlStreamReader>
#include "../libEet/eetv4xml.h"
class EetV4XmlTest : public QObject
{
Q_OBJECT
private:
static EetV4SaleRequest validRequest()
{
EetV4SaleRequest request;
request.messageUuid = QUuid(QStringLiteral("550e8400-e29b-41d4-a716-446655440000"));
request.sentAt = QDateTime(QDate(2027, 1, 1), QTime(12, 0),
QTimeZone::fromSecondsAheadOfUtc(3600));
request.saleAt = request.sentAt;
request.taxpayerEic = QStringLiteral("CZ00000019");
request.unitId = 11;
request.cashRegisterId = QStringLiteral("1");
request.receiptNumber = QStringLiteral("123");
EetV4Amount::fromString(QStringLiteral("123.45"), &request.totalSale);
return request;
}
private slots:
void serializesRequiredAttributes()
{
QString error;
const QByteArray xml = EetV4Xml::serializeSale(validRequest(), &error);
QVERIFY2(!xml.isEmpty(), qPrintable(error));
QVERIFY(xml.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"));
QVERIFY(xml.contains("xmlns=\"http://fs.gov.cz/eet/schema/v4\""));
QVERIFY(xml.contains("uuid_zpravy=\"550e8400-e29b-41d4-a716-446655440000\""));
QVERIFY(xml.contains("eic_popl=\"CZ00000019\""));
QVERIFY(xml.contains("id_jednotky=\"11\""));
QVERIFY(xml.contains("celk_trzba=\"123.45\""));
QVERIFY(!xml.contains("fik"));
QVERIFY(!xml.contains("pkp"));
QVERIFY(!xml.contains("bkp"));
}
void serializesOptionalAttributes()
{
EetV4SaleRequest request = validRequest();
request.verificationMode = true;
request.authorizingTaxpayerEic = QStringLiteral("CZ00000020");
request.multipleTaxpayerAuthorization = true;
request.hasIntendedSettlement = true;
request.hasSettlement = true;
EetV4Amount::fromString(QStringLiteral("100.00"), &request.intendedSettlement);
EetV4Amount::fromString(QStringLiteral("25.00"), &request.settlement);
const QByteArray xml = EetV4Xml::serializeSale(request);
QVERIFY(xml.contains("overeni=\"true\""));
QVERIFY(xml.contains("eic_poverujiciho=\"CZ00000020\""));
QVERIFY(xml.contains("povereni_vice_popl=\"true\""));
QVERIFY(xml.contains("urceno_cerp_zuct=\"100.00\""));
QVERIFY(xml.contains("cerp_zuct=\"25.00\""));
}
void rejectsInvalidRequest()
{
EetV4SaleRequest request = validRequest();
request.taxpayerEic = QStringLiteral("invalid");
QString error;
QVERIFY(EetV4Xml::serializeSale(request, &error).isEmpty());
QVERIFY(!error.isEmpty());
}
void producesParseableXml()
{
QXmlStreamReader reader(EetV4Xml::serializeSale(validRequest()));
while (!reader.atEnd())
{
reader.readNext();
}
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
}
};
QTEST_MAIN(EetV4XmlTest)
#include "eetv4xml_test.moc"