diff --git a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java index 75639014..b036e61e 100644 --- a/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java +++ b/src/main/java/ee/sk/smartid/AuthenticationResponseMapperImpl.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS + * Copyright (C) 2018 - 2026 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -220,14 +220,14 @@ private static void validateSignatureAlgorithmParameters(SessionSignature sessio } Optional maskGenHashAlgorithm = HashAlgorithm.fromString(maskGenAlgorithm.getParameters().getHashAlgorithm()); if (maskGenHashAlgorithm.isEmpty()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: {}", maskGenAlgorithm.getParameters().getHashAlgorithm()); throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value"); } if (hashAlgorithm.get() != maskGenHashAlgorithm.get()) { - logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", + logger.error("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: {}, actual: {}", hashAlgorithm.get().getAlgorithmName(), maskGenHashAlgorithm.get().getAlgorithmName()); - throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); + throw new UnprocessableSmartIdResponseException("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value"); } if (signatureAlgorithmParameters.getSaltLength() == null) { diff --git a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java index a671136a..a2b5bb2a 100644 --- a/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java +++ b/src/test/java/ee/sk/smartid/AuthenticationResponseMapperImplTest.java @@ -4,7 +4,7 @@ * #%L * Smart ID sample Java client * %% - * Copyright (C) 2018 - 2025 SK ID Solutions AS + * Copyright (C) 2018 - 2026 SK ID Solutions AS * %% * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -30,9 +30,11 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import ch.qos.logback.classic.Level; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; @@ -49,11 +51,18 @@ import ee.sk.smartid.rest.dao.SessionSignature; import ee.sk.smartid.rest.dao.SessionSignatureAlgorithmParameters; import ee.sk.smartid.rest.dao.SessionStatus; +import ee.sk.smartid.testhelper.log.Logs; +import ee.sk.smartid.testhelper.log.LogsSpy; +import ee.sk.smartid.testhelper.log.LogsSpyExtension; +@ExtendWith(LogsSpyExtension.class) class AuthenticationResponseMapperImplTest { private static final String AUTH_CERT = FileUtil.readFileToString("test-certs/auth-cert-40504040001.pem.crt"); + @Logs + private LogsSpy logs; + private AuthenticationResponseMapper authenticationResponseMapper; @BeforeEach @@ -439,6 +448,9 @@ void from_hashAlgorithmIsInvalid_throwException(String invalidHashAlgorithm) { var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has invalid value: " + invalidHashAlgorithm); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.hashAlgorithm' has unsupported value", exception.getMessage()); } @@ -487,6 +499,9 @@ void from_algorithmValueInMaskGenAlgorithmIsInvalid_throwException() { var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has invalid value: " + maskGenAlgorithm.getAlgorithm()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm' has unsupported value", exception.getMessage()); } @@ -549,6 +564,9 @@ void from_hashAlgorithmInMaskGenAlgorithmParametersInvalid_throwException(String var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has invalid value: " + maskGenAlgorithmParameters.getHashAlgorithm()); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' has unsupported value", exception.getMessage()); } @@ -570,7 +588,10 @@ void from_hashAlgorithmInMaskGenAlgorithmDoesNotMatchSignaturesHashAlgorithm_thr var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); - assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' and 'signature.signatureAlgorithmParameters.hashAlgorithm' do not match. Expected: SHA3-512, actual: SHA-512"); + + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.maskGenAlgorithm.parameters.hashAlgorithm' value does not match 'signature.signatureAlgorithmParameters.hashAlgorithm' value", exception.getMessage()); } @Test @@ -608,6 +629,9 @@ void from_saltLengthDoesNotMatchHashAlgorithmOctetLength_throwException() { var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value. Expected: 64, actual: 20"); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.saltLength' has invalid value", exception.getMessage()); } @@ -649,6 +673,9 @@ void from_trailerFieldValueIsInvalid_throwException() { var sessionStatus = toSessionStatus(sessionResult, sessionSignature); var exception = assertThrows(UnprocessableSmartIdResponseException.class, () -> authenticationResponseMapper.from(sessionStatus)); + + logs.shouldHave(Level.ERROR, "Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has invalid value: invalid"); + assertEquals("Authentication session status field 'signature.signatureAlgorithmParameters.trailerField' has unsupported value", exception.getMessage()); } diff --git a/src/test/java/ee/sk/smartid/testhelper/log/Logs.java b/src/test/java/ee/sk/smartid/testhelper/log/Logs.java new file mode 100644 index 00000000..89d5c5d4 --- /dev/null +++ b/src/test/java/ee/sk/smartid/testhelper/log/Logs.java @@ -0,0 +1,39 @@ +package ee.sk.smartid.testhelper.log; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2026 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(LogsSpyExtension.class) +public @interface Logs {} diff --git a/src/test/java/ee/sk/smartid/testhelper/log/LogsSpy.java b/src/test/java/ee/sk/smartid/testhelper/log/LogsSpy.java new file mode 100644 index 00000000..19808efa --- /dev/null +++ b/src/test/java/ee/sk/smartid/testhelper/log/LogsSpy.java @@ -0,0 +1,94 @@ +package ee.sk.smartid.testhelper.log; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2026 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.function.Predicate; + +public final class LogsSpy { + + private Logger logger; + private ListAppender appender; + private Level initialLevel; + + void prepare() { + appender = new ListAppender<>(); + logger = (Logger) LoggerFactory.getLogger("ee.sk.smartid"); + logger.addAppender(appender); + initialLevel = logger.getLevel(); + logger.setLevel(Level.TRACE); + appender.start(); + } + + void reset() { + logger.setLevel(initialLevel); + logger.detachAppender(appender); + } + + public LogsSpy shouldHave(Level level, String content) { + boolean found = logEvents().stream().anyMatch(withLog(level, content)); + assertTrue(found, "Expected at least one log entry with level " + level + " and content: " + content); + + return this; + } + + public LogsSpy shouldHave(Level level, String content, int count) { + long actualCount = logEvents().stream() + .filter(withLog(level, content)) + .count(); + + assertEquals(count, actualCount, "Expected " + count + " log entries with level " + level + " and content: " + content); + + return this; + } + + public LogsSpy shouldNotHave(Level level, String content) { + boolean found = logEvents().stream().anyMatch(withLog(level, content)); + assertFalse(found, "Expected no log entries with level " + level + " and content: " + content); + + return this; + } + + private List logEvents() { + // Copy the list to avoid concurrent modification exceptions + return List.copyOf(appender.list); + } + + private Predicate withLog(Level level, String content) { + return event -> level.equals(event.getLevel()) && event.toString().contains(content); + } +} diff --git a/src/test/java/ee/sk/smartid/testhelper/log/LogsSpyExtension.java b/src/test/java/ee/sk/smartid/testhelper/log/LogsSpyExtension.java new file mode 100644 index 00000000..2fa41c38 --- /dev/null +++ b/src/test/java/ee/sk/smartid/testhelper/log/LogsSpyExtension.java @@ -0,0 +1,94 @@ +package ee.sk.smartid.testhelper.log; + +/*- + * #%L + * Smart ID sample Java client + * %% + * Copyright (C) 2018 - 2026 SK ID Solutions AS + * %% + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * #L% + */ + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.TestInstancePostProcessor; +import org.junit.platform.commons.support.ModifierSupport; + +import java.lang.reflect.Field; +import java.util.function.Predicate; + +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; + +public final class LogsSpyExtension + implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, ParameterResolver, TestInstancePostProcessor { + + private final LogsSpy logsSpy = new LogsSpy(); + + @Override + public void beforeEach(ExtensionContext context) { + logsSpy.prepare(); + } + + @Override + public void afterEach(ExtensionContext context) { + logsSpy.reset(); + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return parameterContext.getParameter().getType().equals(LogsSpy.class); + } + + @Override + public LogsSpy resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) + throws ParameterResolutionException { + return logsSpy; + } + + @Override + public void beforeAll(ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + injectFields(testClass, null, ModifierSupport::isStatic); + } + + @Override + public void postProcessTestInstance(Object testInstance, ExtensionContext context) { + Class testClass = context.getRequiredTestClass(); + injectFields(testClass, testInstance, ModifierSupport::isNotStatic); + } + + private void injectFields(Class testClass, Object testInstance, Predicate predicate) { + predicate = predicate.and(field -> LogsSpy.class.isAssignableFrom(field.getType())); + findAnnotatedFields(testClass, Logs.class, predicate).forEach(field -> { + try { + field.setAccessible(true); + field.set(testInstance, logsSpy); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } +}