From 1bce7db64e16c168739a2fbe22c392054c23423a Mon Sep 17 00:00:00 2001 From: Ralph Soika Date: Sun, 16 Nov 2025 11:03:05 +0100 Subject: [PATCH] logging --- .../ksef/api/KSeFAPIService.java | 16 +- .../ksef/api/KSeFAuthManager.java | 415 +++++++++++++----- .../ksef/api/KSeFAPIServiceTest.java | 3 +- .../ksef/api/KSeFAuthManagerTest.java | 282 ++++++------ .../src/test/resources/ksef/test.json | 129 +++++- 5 files changed, 586 insertions(+), 259 deletions(-) diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java index da44761..5d5f76a 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAPIService.java @@ -25,7 +25,6 @@ import org.imixs.workflow.ItemCollection; import org.imixs.workflow.engine.DocumentService; import org.imixs.workflow.exceptions.PluginException; -import jakarta.annotation.PostConstruct; import jakarta.annotation.security.DeclareRoles; import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RunAs; @@ -60,11 +59,6 @@ public class KSeFAPIService { @Inject DocumentService documentService; - @PostConstruct - void init() { - - } - /** * This method uploads an KSeF Invoice document (XML). * The response of the uplaod is a 'referenceNumber' which is stored into the @@ -117,10 +111,7 @@ public class KSeFAPIService { IvParameterSpec iv = new IvParameterSpec(ivBytes); // ---------- 4) XML per AES-256-CBC verschlüsseln ---------- - javax.crypto.Cipher aesCipher; - - aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); - + javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv); byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml); @@ -198,6 +189,9 @@ public class KSeFAPIService { } catch (Exception e) { logger.severe("├── ⚠️ Error parsing JSON response" + e.getMessage()); + + kseFAuthManager.deleteCurrentSession(); + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, "Error parsing JSON response: " + e.getMessage()); @@ -220,6 +214,8 @@ public class KSeFAPIService { fileData.setAttribute("ksef.referenceNumber", textlist); kseFAuthManager.closeSession(); + kseFAuthManager.deleteCurrentSession(); + return referenceNumber; } diff --git a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java index 6d34314..150d476 100644 --- a/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java +++ b/office-alexander-logistics-app/src/main/java/com/alexanderlogistics/ksef/api/KSeFAuthManager.java @@ -15,7 +15,9 @@ import java.security.spec.MGF1ParameterSpec; import java.time.Duration; import java.time.Instant; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Base64; +import java.util.List; import java.util.Optional; import java.util.logging.Logger; @@ -36,6 +38,7 @@ import jakarta.ejb.Lock; import jakarta.ejb.LockType; import jakarta.ejb.Singleton; import jakarta.inject.Inject; +import jakarta.json.JsonArray; import jakarta.json.JsonObject; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; @@ -195,14 +198,22 @@ public class KSeFAuthManager { logger.info("│ ├── open new interactive KSeF Session..."); // open new session this.loadPublicKeyCertificates(); - this.authChallenge(); this.authKSeFToken(); + this.waitSomeTime(3000); + + // Jetzt Status abwarten + this.waitForAuthStatus(); + + waitSomeTime(5000); + this.redeemToken(); + + this.openInteractiveSession(); + // Delete deprecated sessions + this.deleteDeprecatedSessions(); this.checkTokenStatus(); - this.redeemToken(); - this.openInteractiveSession(); } else { logger.warning("│ ├── ! Session already exists - reuse existing KSeF Session..."); } @@ -262,10 +273,8 @@ public class KSeFAuthManager { logger.info("├── ♻️ KSeF API load public-key-certificates..."); String uri = baseURI + "/security/public-key-certificates"; + logger.info("│ ├── GET: " + uri); - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) .header("Accept", "application/json") @@ -279,9 +288,9 @@ public class KSeFAuthManager { throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, "Unable to send request for KSeF certificates endpoint: " + e.getMessage()); } - + logger.info("│ ├── HTTP Response: " + response.statusCode()); if (debug) { - logger.info("│ ├── Response Code = " + response.statusCode()); + logger.info("Response: " + response.body()); } @@ -342,9 +351,7 @@ public class KSeFAuthManager { logger.info("├── 🛅 KSeF API Challenge..."); String uri = baseURI + "/auth/challenge"; - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } + logger.info("│ ├── POST: " + uri); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(uri)) // nur Endpoint checken .header("Accept", "application/json") @@ -354,8 +361,9 @@ public class KSeFAuthManager { HttpResponse response; try { response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + logger.info("│ ├── HTTP Response: " + response.statusCode()); if (debug) { - logger.info("│ ├── Response Code = " + response.statusCode()); + logger.info("Response: " + response.body()); } // Prüfen, dass wir eine API-Antwort bekommen, kein HTML @@ -400,9 +408,8 @@ public class KSeFAuthManager { logger.info("├── 🛃 KSeF API auth ksef-token..."); String uri = baseURI + "/auth/ksef-token"; - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } + logger.info("│ ├── POST: " + uri); + HttpResponse response = null; try { String jsonPayload = String.format( @@ -429,8 +436,8 @@ public class KSeFAuthManager { "API Error: Unable to request ksef-token"); } + logger.info("│ ├── HTTP Response: " + response.statusCode()); if (debug) { - logger.info("│ ├── Response Code = " + response.statusCode()); logger.info("Response: " + response.body()); } @@ -463,6 +470,113 @@ public class KSeFAuthManager { } } + /** + * Wartet auf den Korrekten Auth Status... + * + * @throws PluginException + */ + public void waitForAuthStatus() throws PluginException { + logger.info("├── 🔜 Waiting for Auth status 200..."); + + // Now we need to wait until auth/{AuthRef} will return 200 + long start = System.currentTimeMillis(); + long timeout = 10_000; // 10 Sekunden + + while (System.currentTimeMillis() - start < timeout) { + if (this.checkTokenStatus() == 200) { + break; // Erfolg + } + // kleine Pause, um CPU zu schonen + waitSomeTime(200); + + } + + // Optional: prüfen, ob Timeout erreicht wurde + if (System.currentTimeMillis() - start >= timeout) { + logger.info("├── ⚠️ Timeout reached after 10 seconds!"); + } + + } + + /** + * waits some time.. + */ + private void waitSomeTime(long millis) { + logger.info("│ ├── waiting " + millis + "ms...."); + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Redeems the KSeF token and returns a session access token to be used to open + * an Interactive Session + * + * POST /auth/token/redeem + * + * @return JsonObject with referenceNumber and sessionToken + * @throws Exception + */ + @Lock(LockType.WRITE) + public JsonObject redeemToken() throws PluginException { + + logger.info("├── 🛂 KSeF API redeem token..."); + String uri = baseURI + "/auth/token/redeem"; + + logger.info("│ ├── POST: " + uri); + + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", "application/json") + .header("Authorization", "Bearer " + authToken) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + + response = httpClient.send(request, + HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error - unable to redeemToken: " + e.getMessage()); + } + + logger.info("│ ├── HTTP Response: " + response.statusCode()); + if (debug) { + logger.info("│ ├── Response: " + response.body()); + } + + if (response.statusCode() >= 400) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), + ERROR_API, + "Token redeem failed: " + response.statusCode() + + " - " + response.body()); + } + + try (Jsonb jsonb = JsonbBuilder.create()) { + JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); + + // Access Token extrahieren und speichern + JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken"); + this.accessToken = accessTokenObj.getString("token"); + String validUntil = accessTokenObj.getString("validUntil"); + + logger.info("│ ├── ✓ Access Token received"); + logger.info("│ └── ✓ validUntil: " + validUntil); + + return jsonObject; + + } catch (Exception e) { + logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage()); + throw new PluginException(KSeFAuthManager.class.getSimpleName(), + ERROR_API, + "Error parsing response: " + e.getMessage()); + } + } + /** * Helper Method to open an interactive session. The method fetches a * sessionRefNumber which is mandatory to upload an invoice. @@ -479,9 +593,8 @@ public class KSeFAuthManager { logger.info("├── 🔐 KSeF API open interactive session..."); String uri = baseURI + "/sessions/online"; - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } + logger.info("│ ├── POST: " + uri); + HttpResponse response = null; try { // --- AES Schlüssel erzeugen --- @@ -529,8 +642,9 @@ public class KSeFAuthManager { response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + logger.info("│ ├── HTTP Response: " + response.statusCode()); if (debug) { - logger.info("│ ├── Response Code = " + response.statusCode()); + logger.info("Response: " + response.body()); } @@ -546,8 +660,8 @@ public class KSeFAuthManager { sessionRefNumber = jsonObject.getString("referenceNumber"); sessionValidUntil = jsonObject.getString("validUntil"); if (true) { - logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber); - logger.info("│ └── Session ValidUntil = " + sessionValidUntil); + logger.info("│ ├── Session ReferenceNumber = " + sessionRefNumber); + logger.info("│ └── Session ValidUntil = " + sessionValidUntil); } // IMPORTANT: Store the AES key - this key is mandatory to upload a invoice @@ -566,6 +680,14 @@ public class KSeFAuthManager { "API Error openInteractiveSession: " + e.getMessage()); } + // logger.info("│ ├── Sleep 5 sec..."); + // try { + // Thread.sleep(5000); + // } catch (InterruptedException e) { + // Thread.currentThread().interrupt(); + + // } + } /** @@ -598,8 +720,6 @@ public class KSeFAuthManager { long timestampMillis = instant.toEpochMilli(); - logger.info("│ ├── Timestamp (ms): " + timestampMillis); - // 2. Format erstellen: token|timestamp String plaintext = ksefToken.get() + "|" + timestampMillis; @@ -634,71 +754,6 @@ public class KSeFAuthManager { return result; } - /** - * Redeems the KSeF token and returns a session access token to be used to open - * an Interactive Session - * - * POST /auth/token/redeem - * - * @return JsonObject with referenceNumber and sessionToken - * @throws Exception - */ - @Lock(LockType.WRITE) - public JsonObject redeemToken() throws PluginException { - - logger.info("├── 🛂 KSeF API redeem token..."); - String uri = baseURI + "/auth/token/redeem"; - - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } - HttpResponse response = null; - try { - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("Accept", "application/json") - .header("Authorization", "Bearer " + authToken) - .POST(HttpRequest.BodyPublishers.noBody()) - .build(); - - response = httpClient.send(request, - HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { - throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, - "API Error - unable to redeemToken: " + e.getMessage()); - } - - if (debug) { - logger.info("│ ├── Response Code = " + response.statusCode()); - logger.info("│ ├── Response: " + response.body()); - } - - if (response.statusCode() >= 400) { - throw new PluginException(KSeFAuthManager.class.getSimpleName(), - ERROR_API, - "Token redeem failed: " + response.statusCode() - + " - " + response.body()); - } - - try (Jsonb jsonb = JsonbBuilder.create()) { - JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); - - // Access Token extrahieren und speichern - JsonObject accessTokenObj = jsonObject.getJsonObject("accessToken"); - this.accessToken = accessTokenObj.getString("token"); - - logger.info("│ └── ☑️ Access Token received"); - - return jsonObject; - - } catch (Exception e) { - logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage()); - throw new PluginException(KSeFAuthManager.class.getSimpleName(), - ERROR_API, - "Error parsing response: " + e.getMessage()); - } - } - private PublicKey extractPublicKeyFromCertificate(String base64Cert) throws Exception { byte[] der = java.util.Base64.getDecoder().decode(base64Cert); java.security.cert.CertificateFactory factory = java.security.cert.CertificateFactory.getInstance("X.509"); @@ -714,15 +769,15 @@ public class KSeFAuthManager { * * https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get * + * @return HTTP Response Code * @throws PluginException */ - public void checkTokenStatus() throws PluginException { - logger.info("├── 🌡️ KSeF API check auth status..."); + public int checkTokenStatus() throws PluginException { + logger.info("├── ↩️ KSeF API check auth status..."); String uri = baseURI + "/auth/" + authRefNumber; + int httpResponse = -1; - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } + logger.info("│ ├── GET: " + uri); HttpResponse response = null; try { HttpRequest request = HttpRequest.newBuilder() @@ -737,10 +792,11 @@ public class KSeFAuthManager { throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, "API Error: Unable to request auth status: " + e.getMessage()); } - if (true) { - logger.info("│ ├── Response Code = " + response.statusCode()); - logger.info("Response: " + response.body()); - } + + httpResponse = response.statusCode(); + logger.info("│ ├── HTTP Response: " + httpResponse); + + logger.info("│ ├── " + response.body()); try (Jsonb jsonb = JsonbBuilder.create()) { JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); @@ -767,6 +823,163 @@ public class KSeFAuthManager { throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, "Error parsing JSON response: " + e.getMessage()); } + + return httpResponse; + } + + /** + * This method returns all active sessions + * + * https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Aktywne-sesje/paths/~1api~1v2~1auth~1sessions/get + * + * @throws PluginException + */ + public JsonObject getActiveSessions() throws PluginException { + logger.info("├── 🚸 KSeF API active sessions..."); + String uri = baseURI + "/auth/sessions"; + + if (debug) { + logger.info("│ ├── GET: " + uri); + } + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .GET() + .build(); + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error: Unable to request auth status: " + e.getMessage()); + } + + if (true) { + logger.info("│ ├── HTTP Response: " + response.statusCode()); + // logger.info("Response: " + response.body()); + } + try (Jsonb jsonb = JsonbBuilder.create()) { + JsonObject jsonObject = jsonb.fromJson(response.body(), JsonObject.class); + return jsonObject; + + } catch (Exception e) { + logger.severe("├── ⚠️ Error parsing JSON response: " + e.getMessage()); + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "Error parsing JSON response: " + e.getMessage()); + } + + } + + /** + * Deletes all old sessions + * + * The method first asks for old sessions and if we have more then 2 sessions we + * delete the deprecated ones. + * + * @param sessionData + * @throws PluginException + */ + public void deleteDeprecatedSessions() throws PluginException { + int maxIterations = 10; // Sicherheits-Begrenzung + + for (int iteration = 0; iteration < maxIterations; iteration++) { + JsonObject currentSessions = this.getActiveSessions(); + + // Get list of refNumbers + List referenceNumbers = new ArrayList<>(); + + JsonArray items = currentSessions.getJsonArray("items"); + for (int i = 0; i < items.size(); i++) { + JsonObject item = items.getJsonObject(i); + referenceNumbers.add(item.getString("referenceNumber")); + } + + // Wenn nur noch eine Session da ist, abbrechen + if (referenceNumbers.size() <= 1) { + if (iteration == 0) { + logger.info("│ ├── Only one session found, no deprecated sessions"); + } else { + logger.info("├── 🎉 All deprecated sessions deleted after " + iteration + " iterations"); + } + break; + } + + // delete old sessions.... + logger.info("├── 🚮 KSeF API delete deprecated sessions (iteration " + (iteration + 1) + ")..."); + String uri = baseURI + "/auth/sessions/"; + + // Alle Einträge ab Index 1 (also ab dem zweiten Eintrag) durchgehen + for (int i = 1; i < referenceNumbers.size(); i++) { + String referenceNumber = referenceNumbers.get(i); + logger.info("│ ├── delete deprecated session: " + referenceNumber); + + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri + referenceNumber)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error: Unable to request auth status: " + e.getMessage()); + } + + logger.info("│ ├── HTTP Response: " + response.statusCode()); + } + + logger.info("│ ├── Deleted " + (referenceNumbers.size() - 1) + " sessions in this iteration"); + + // Kurze Pause vor der nächsten Iteration + if (referenceNumbers.size() > 1 && iteration < maxIterations - 1) { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + + // Wenn wir die maximale Iteration erreicht haben aber immer noch Sessions da + // sind + if (iteration == maxIterations - 1 && referenceNumbers.size() > 1) { + logger.warning("├── ⚠️ Maximum iterations reached, but " + (referenceNumbers.size() - 1) + + " sessions still remain"); + } + } + } + + /** + * Deletes the current session + * + * @throws PluginException + */ + public void deleteCurrentSession() throws PluginException { + + logger.info("├── 🚮 KSeF API delete current session..."); + String uri = baseURI + "/auth/sessions/current"; + + HttpResponse response = null; + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Accept", "application/json") + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + accessToken) + .DELETE() + .build(); + response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, + "API Error: Unable to request auth status: " + e.getMessage()); + } + + logger.info("│ ├── HTTP Response: " + response.statusCode()); } /** @@ -785,9 +998,7 @@ public class KSeFAuthManager { // https://ksef-test.mf.gov.pl/api/v2/sessions/online/{referenceNumber}/close String uri = baseURI + "/sessions/online/" + sessionRefNumber + "/close"; - if (debug) { - logger.info("│ ├── Endpoint: " + uri); - } + logger.info("│ ├── POST: " + uri); if (sessionRefNumber == null || sessionRefNumber.isEmpty()) { throw new PluginException(KSeFAuthManager.class.getSimpleName(), ERROR_API, @@ -809,7 +1020,7 @@ public class KSeFAuthManager { "API Error: Unable to request auth status: " + e.getMessage()); } if (true) { - logger.info("│ ├── Response Code = " + response.statusCode()); + logger.info("│ ├── HTTP Response: " + response.statusCode()); logger.info("Response: " + response.body()); } diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java index e6ec91a..67a520a 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAPIServiceTest.java @@ -13,7 +13,6 @@ import java.util.logging.Logger; import org.imixs.workflow.FileData; import org.imixs.workflow.ItemCollection; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -24,7 +23,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import com.alexanderlogistics.TestLoggerConfig; -@Disabled +//@Disabled @ExtendWith(MockitoExtension.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class KSeFAPIServiceTest { diff --git a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java index e48b8a4..f15b26b 100644 --- a/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java +++ b/office-alexander-logistics-app/src/test/java/com/alexanderlogistics/ksef/api/KSeFAuthManagerTest.java @@ -18,7 +18,6 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; @@ -27,155 +26,160 @@ import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; -@Disabled +import com.alexanderlogistics.TestLoggerConfig; + @ExtendWith(MockitoExtension.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class KSeFAuthManagerTest { - private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName()); + private static Logger logger = Logger.getLogger(KSeFAuthManagerTest.class.getName()); - private KSeFAuthManager manager; + private KSeFAuthManager manager; - @BeforeEach - void setup() throws Exception { - manager = new KSeFAuthManager(); + @BeforeEach + void setup() throws Exception { + TestLoggerConfig.setupTestLogger(); + manager = new KSeFAuthManager(); - // Test config - manager.setKsefToken(Params.TOKEN); - manager.setKsefNip(Params.NIP); - manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2"); - manager.setDebug(false); + // Test config + manager.setKsefToken(Params.TOKEN); + manager.setKsefNip(Params.NIP); + manager.setKsefEndpoint("https://ksef-test.mf.gov.pl/api/v2"); + manager.setDebug(false); - // init() ausführen - manager.init(); + // init() ausführen + manager.init(); + } + + // ------------------------------------------------------------ + // 1) Challenge abholen + // ------------------------------------------------------------ + @Test + @Order(1) + @DisplayName("POST /api/v2/auth/token/redeem") + void testRedeemToken() throws Exception { + + manager.loadPublicKeyCertificates(); + manager.authChallenge(); + manager.authKSeFToken(); + manager.redeemToken(); + + assertNotNull( + manager.getAuthRefNumber(), + "RefNumber darf nicht NULL sein"); + + assertNotNull( + manager.getAuthToken(), + "AuthToken darf nicht leer sein"); + + } + + @Test + @Order(4) + @DisplayName("Upload Invoice XML (AES-256 + RSA)") + public void testUploadInvoice() throws Exception { + + // ---------- 1) Vorbereitung ---------- + // API initialisieren + manager.loadPublicKeyCertificates(); + manager.authChallenge(); + manager.authKSeFToken(); + + manager.waitForAuthStatus(); + + manager.redeemToken(); + + manager.openInteractiveSession(); + + assertNotNull(manager.getAccessToken(), "AccessToken fehlt"); + // assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt"); + assertNotNull(manager.getSessionRefNumber(), "SessionReferenceNumber fehlt"); + + // ---------- 2) XML laden ---------- + Path xmlPath = Paths.get("src/test/resources/ksef/example-invoice-01.xml"); + byte[] invoiceXml = Files.readAllBytes(xmlPath); + + // ---------- 3) AES Key generieren ---------- + SecureRandom random = new SecureRandom(); + + byte[] aesKeyBytes = new byte[32]; // 256-bit AES key + random.nextBytes(aesKeyBytes); + + byte[] ivBytes = new byte[16]; // 128-bit IV + random.nextBytes(ivBytes); + + SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); + IvParameterSpec iv = new IvParameterSpec(ivBytes); + + // ---------- 4) XML per AES-256-CBC verschlüsseln ---------- + javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); + aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv); + + byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml); + + // ---------- 5) AES Key per RSA verschlüsseln ---------- + PublicKey ksefPublicKey = manager.getSymmetricPublicKey(); + + javax.crypto.Cipher rsaCipher = javax.crypto.Cipher + .getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); + rsaCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, ksefPublicKey); + + byte[] encryptedAesKey = rsaCipher.doFinal(aesKeyBytes); + + // ---------- 6) SHA-256 Hash des Klartext-XML berechnen ---------- + MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); + + // hash origin invoice + byte[] xmlHashBytes = sha256.digest(invoiceXml); + String invoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); + + // hash encrypted invoice + xmlHashBytes = sha256.digest(encryptedInvoiceXml); + String encryptedInvoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); + + // ---------- 7) Request-Body bauen ---------- + // Struktur gemäß KSeF-Doku + String jsonBody = String.format("{ " + + "\"invoiceHash\": \"%s\", " + + "\"invoiceSize\": \"%s\", " + + "\"encryptedInvoiceHash\": \"%s\", " + + "\"encryptedInvoiceSize\": \"%s\", " + + "\"encryptedInvoiceContent\": \"%s\", " + + "\"offlineMode\": false " + + "}", + invoiceHash, + invoiceXml.length, + encryptedInvoiceHash, + encryptedInvoiceXml.length, + Base64.getEncoder().encodeToString(encryptedInvoiceXml)); + + // ---------- 8) HTTP POST vorbereiten ---------- + // String uri = manager.getBaseURI() + "/sessions/online/" + + // manager.getRefNumber() + "/invoices"; + String uri = manager.getBaseURI() + "/sessions/online/" + manager.getSessionRefNumber() + "/invoices"; + logger.info("│ ├── Endpoint: " + uri); + + if (manager.isDebug()) { + logger.info("Request: " + jsonBody); } + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(uri)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + manager.getAccessToken()) + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .build(); - // ------------------------------------------------------------ - // 1) Challenge abholen - // ------------------------------------------------------------ - @Test - @Order(1) - @DisplayName("POST /api/v2/auth/token/redeem") - void testRedeemToken() throws Exception { + // ---------- 9) Call ausführen ---------- + var response = manager.getHttpClient().send(request, + java.net.http.HttpResponse.BodyHandlers.ofString()); - manager.loadPublicKeyCertificates(); - manager.authChallenge(); - manager.authKSeFToken(); - manager.redeemToken(); + System.out.println("Upload Response:"); + System.out.println(response.statusCode()); + System.out.println(response.body()); - assertNotNull( - manager.getAuthRefNumber(), - "RefNumber darf nicht NULL sein"); - - assertNotNull( - manager.getAuthToken(), - "AuthToken darf nicht leer sein"); - - } - - @Test - @Order(4) - @DisplayName("Upload Invoice XML (AES-256 + RSA)") - public void testUploadInvoice() throws Exception { - - // ---------- 1) Vorbereitung ---------- - // API initialisieren - manager.loadPublicKeyCertificates(); - manager.authChallenge(); - manager.authKSeFToken(); - manager.redeemToken(); - - manager.openInteractiveSession(); - - assertNotNull(manager.getAccessToken(), "AccessToken fehlt"); - // assertNotNull(manager.getRefNumber(), "ReferenceNumber fehlt"); - assertNotNull(manager.getSessionRefNumber(), "SessionReferenceNumber fehlt"); - - // ---------- 2) XML laden ---------- - Path xmlPath = Paths.get("src/test/resources/ksef/example-invoice-01.xml"); - byte[] invoiceXml = Files.readAllBytes(xmlPath); - - // ---------- 3) AES Key generieren ---------- - SecureRandom random = new SecureRandom(); - - byte[] aesKeyBytes = new byte[32]; // 256-bit AES key - random.nextBytes(aesKeyBytes); - - byte[] ivBytes = new byte[16]; // 128-bit IV - random.nextBytes(ivBytes); - - SecretKeySpec aesKey = new SecretKeySpec(aesKeyBytes, "AES"); - IvParameterSpec iv = new IvParameterSpec(ivBytes); - - // ---------- 4) XML per AES-256-CBC verschlüsseln ---------- - javax.crypto.Cipher aesCipher = javax.crypto.Cipher.getInstance("AES/CBC/PKCS5Padding"); - aesCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, aesKey, iv); - - byte[] encryptedInvoiceXml = aesCipher.doFinal(invoiceXml); - - // ---------- 5) AES Key per RSA verschlüsseln ---------- - PublicKey ksefPublicKey = manager.getSymmetricPublicKey(); - - javax.crypto.Cipher rsaCipher = javax.crypto.Cipher - .getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); - rsaCipher.init(javax.crypto.Cipher.ENCRYPT_MODE, ksefPublicKey); - - byte[] encryptedAesKey = rsaCipher.doFinal(aesKeyBytes); - - // ---------- 6) SHA-256 Hash des Klartext-XML berechnen ---------- - MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - - // hash origin invoice - byte[] xmlHashBytes = sha256.digest(invoiceXml); - String invoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); - - // hash encrypted invoice - xmlHashBytes = sha256.digest(encryptedInvoiceXml); - String encryptedInvoiceHash = Base64.getEncoder().encodeToString(xmlHashBytes); - - // ---------- 7) Request-Body bauen ---------- - // Struktur gemäß KSeF-Doku - String jsonBody = String.format("{ " + - "\"invoiceHash\": \"%s\", " + - "\"invoiceSize\": \"%s\", " + - "\"encryptedInvoiceHash\": \"%s\", " + - "\"encryptedInvoiceSize\": \"%s\", " + - "\"encryptedInvoiceContent\": \"%s\", " + - "\"offlineMode\": false " + - "}", - invoiceHash, - invoiceXml.length, - encryptedInvoiceHash, - encryptedInvoiceXml.length, - Base64.getEncoder().encodeToString(encryptedInvoiceXml)); - - // ---------- 8) HTTP POST vorbereiten ---------- - // String uri = manager.getBaseURI() + "/sessions/online/" + - // manager.getRefNumber() + "/invoices"; - String uri = manager.getBaseURI() + "/sessions/online/" + manager.getSessionRefNumber() + "/invoices"; - logger.info("│ ├── Endpoint: " + uri); - - if (manager.isDebug()) { - logger.info("Request: " + jsonBody); - } - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(uri)) - .header("Content-Type", "application/json") - .header("Authorization", "Bearer " + manager.getAccessToken()) - .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) - .build(); - - // ---------- 9) Call ausführen ---------- - var response = manager.getHttpClient().send(request, - java.net.http.HttpResponse.BodyHandlers.ofString()); - - System.out.println("Upload Response:"); - System.out.println(response.statusCode()); - System.out.println(response.body()); - - // ---------- 10) Basic Assertions ---------- - assertTrue(response.statusCode() == 202, - "Upload muss erfolgreich sein"); - } + // ---------- 10) Basic Assertions ---------- + assertTrue(response.statusCode() == 202, + "Upload muss erfolgreich sein"); + } } diff --git a/office-alexander-logistics-app/src/test/resources/ksef/test.json b/office-alexander-logistics-app/src/test/resources/ksef/test.json index 8583387..7f6942f 100644 --- a/office-alexander-logistics-app/src/test/resources/ksef/test.json +++ b/office-alexander-logistics-app/src/test/resources/ksef/test.json @@ -1,8 +1,125 @@ { - "invoiceHash": "W5UcWtOuOI7Ebe9nMjGK6Y1sgxKHMRrIwqvc13xanVQ=", - "invoiceSize": "4642", - "encryptedInvoiceHash": "iJug90d6T8AmonxnHWgMA8QA4y5zbKTSIOh1fyAjf8g=", - "encryptedInvoiceSize": "4656", - "encryptedInvoiceContent": "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", - "offlineMode": false + "continuationToken": "W3siY29tcG9zaXRlVG9rZW4iOnsidG9rZW4iOm51bGwsInJhbmdlIjp7Im1pbiI6IjA1QzFFMCIsIm1heCI6IkZGIn19LCJyZXN1bWVWYWx1ZXMiOlsiMjAyNS0xMS0xNVQyMjo0OToxMC4wNzU0OTAzWiJdLCJyaWQiOiIzeHd0QVBJWDVRSC01UzhBQUFBQUJBPT0iLCJza2lwQ291bnQiOjF9XQ==", + "items": [ + { + "referenceNumber": "20251115-AU-4EB2BA3000-379DFBE042-76", + "isCurrent": true, + "startDate": "2025-11-15T22:55:20.987531+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:55:20.987531+00:00" + }, + { + "referenceNumber": "20251115-AU-4EA6F7D000-6099259ADC-61", + "isCurrent": false, + "startDate": "2025-11-15T22:54:32.8211356+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:54:32.8211356+00:00" + }, + { + "referenceNumber": "20251115-AU-4E61444000-609CCF7030-38", + "isCurrent": false, + "startDate": "2025-11-15T22:49:47.3225128+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:47.3225128+00:00" + }, + { + "referenceNumber": "20251115-AU-4E60025000-58907907EE-86", + "isCurrent": false, + "startDate": "2025-11-15T22:49:42.1727969+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:42.1727969+00:00" + }, + { + "referenceNumber": "20251115-AU-4E5EE53000-AFE96AB283-13", + "isCurrent": false, + "startDate": "2025-11-15T22:49:37.6109382+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:37.6109382+00:00" + }, + { + "referenceNumber": "20251115-AU-4E5D1A5000-3796E8F88F-4A", + "isCurrent": false, + "startDate": "2025-11-15T22:49:30.2689947+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:30.2689947+00:00" + }, + { + "referenceNumber": "20251115-AU-4E5BD2F000-58998AE410-DB", + "isCurrent": false, + "startDate": "2025-11-15T22:49:25.0319924+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:25.0319924+00:00" + }, + { + "referenceNumber": "20251115-AU-4E5AA33000-AFEE406A84-2C", + "isCurrent": false, + "startDate": "2025-11-15T22:49:20.1710655+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:20.1710655+00:00" + }, + { + "referenceNumber": "20251115-AU-4E59505000-589BC8294C-8A", + "isCurrent": false, + "startDate": "2025-11-15T22:49:14.7496086+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:14.7496086+00:00" + }, + { + "referenceNumber": "20251115-AU-4E582C3000-2CA95C0350-5E", + "isCurrent": false, + "startDate": "2025-11-15T22:49:10.0754903+00:00", + "authenticationMethod": "Token", + "status": { + "code": 200, + "description": "Uwierzytelnianie zakończone sukcesem" + }, + "isTokenRedeemed": true, + "refreshTokenValidUntil": "2025-11-22T22:49:10.0754903+00:00" + } + ] } \ No newline at end of file