From 3be24b90ef16629fdb680d8e95f70d45c8c268e8 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Tue, 3 Dec 2024 09:36:36 +0100
Subject: [PATCH] OZG-7092 add tests for SamlTokenValidationService

---
 .../saml/SamlTokenValidationService.java      |   9 +-
 .../token/TokenAttributeTestFactory.java      |  28 +
 .../saml/SamlTokenValidationServiceTest.java  | 523 ++++++++++++++++++
 3 files changed, 557 insertions(+), 3 deletions(-)
 create mode 100644 token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java

diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java
index 7e47b27..ef3f560 100644
--- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java
+++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTokenValidationService.java
@@ -25,6 +25,7 @@
 package de.ozgcloud.token.saml;
 
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -144,10 +145,12 @@ public class SamlTokenValidationService {
 	}
 
 	Map<String, String> adjustPostfachIdAttribute(Map<String, String> tokenAttributes, Response token) {
-		if (tokenValidationProperty.isUseIdAsPostfachId()) {
-			tokenAttributes.put(TokenAttribute.POSTFACH_ID_KEY, token.getID());
+		if (!tokenValidationProperty.isUseIdAsPostfachId()) {
+			return tokenAttributes;
 		}
-		return tokenAttributes;
+		var map = new HashMap<>(tokenAttributes);
+		map.put(TokenAttribute.POSTFACH_ID_KEY, token.getID());
+		return map;
 	}
 
 	TokenAttribute buildTokenAttribute(Map.Entry<String, String> attribute) {
diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java
index 5e55108..050d50e 100644
--- a/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java
+++ b/token-checker-server/src/test/java/de/ozgcloud/token/TokenAttributeTestFactory.java
@@ -1,5 +1,30 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
 package de.ozgcloud.token;
 
+import java.util.Map;
+
 import com.thedeanda.lorem.LoremIpsum;
 
 public class TokenAttributeTestFactory {
@@ -17,4 +42,7 @@ public class TokenAttributeTestFactory {
 				.value(VALUE);
 	}
 
+	public static Map<String, String> asMap() {
+		return Map.of(NAME, VALUE);
+	}
 }
diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java
new file mode 100644
index 0000000..7f0fd52
--- /dev/null
+++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTokenValidationServiceTest.java
@@ -0,0 +1,523 @@
+/*
+ * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den
+ * Ministerpräsidenten des Landes Schleswig-Holstein
+ * Staatskanzlei
+ * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung
+ *
+ * Lizenziert unter der EUPL, Version 1.2 oder - sobald
+ * diese von der Europäischen Kommission genehmigt wurden -
+ * Folgeversionen der EUPL ("Lizenz");
+ * Sie dürfen dieses Werk ausschließlich gemäß
+ * dieser Lizenz nutzen.
+ * Eine Kopie der Lizenz finden Sie hier:
+ *
+ * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
+ *
+ * Sofern nicht durch anwendbare Rechtsvorschriften
+ * gefordert oder in schriftlicher Form vereinbart, wird
+ * die unter der Lizenz verbreitete Software "so wie sie
+ * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN -
+ * ausdrücklich oder stillschweigend - verbreitet.
+ * Die sprachspezifischen Genehmigungen und Beschränkungen
+ * unter der Lizenz sind dem Lizenztext zu entnehmen.
+ */
+package de.ozgcloud.token.saml;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Random;
+import java.util.Set;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.opensaml.core.xml.XMLObject;
+import org.opensaml.core.xml.schema.XSAny;
+import org.opensaml.core.xml.schema.XSBoolean;
+import org.opensaml.core.xml.schema.XSBooleanValue;
+import org.opensaml.core.xml.schema.XSInteger;
+import org.opensaml.core.xml.schema.XSString;
+import org.opensaml.saml.saml2.core.Assertion;
+import org.opensaml.saml.saml2.core.Attribute;
+import org.opensaml.saml.saml2.core.AttributeStatement;
+import org.opensaml.saml.saml2.core.EncryptedAssertion;
+import org.opensaml.saml.saml2.core.Response;
+import org.opensaml.saml.saml2.core.Statement;
+import org.opensaml.saml.saml2.encryption.Decrypter;
+import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator;
+import org.opensaml.xmlsec.signature.Signature;
+import org.opensaml.xmlsec.signature.support.SignatureTrustEngine;
+
+import com.thedeanda.lorem.LoremIpsum;
+
+import de.ozgcloud.token.TokenAttribute;
+import de.ozgcloud.token.TokenAttributeTestFactory;
+import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty;
+import de.ozgcloud.token.common.errorhandling.TokenVerificationException;
+import lombok.SneakyThrows;
+import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
+
+class SamlTokenValidationServiceTest {
+
+	@Spy
+	@InjectMocks
+	private SamlTokenValidationService service;
+
+	@Mock
+	private SignatureTrustEngine signatureTrustEngine;
+	@Mock
+	private Decrypter decrypter;
+	@Mock
+	private SAMLSignatureProfileValidator profileValidator;
+	@Mock
+	private TokenValidationProperty tokenValidationProperty;
+	@Mock
+	private CriteriaSet verificationCriteria;
+
+	@Nested
+	class TestValidate {
+
+		private static final Map<String, String> TOKEN_ATTRIBUTES_MAP = Map.of("key", "value");
+		private static final TokenAttribute TOKEN_ATTRIBUTE = TokenAttributeTestFactory.create();
+
+		@Mock
+		private Response token;
+
+		@BeforeEach
+		void init() {
+			doNothing().when(service).validateToken(any());
+			doReturn(TOKEN_ATTRIBUTES_MAP).when(service).decryptAttributes(any());
+			doReturn(Set.of(TOKEN_ATTRIBUTE)).when(service).buildTokenAttributes(any(), any());
+		}
+
+		@Test
+		void shouldCallValidateToken() {
+			validate();
+
+			verify(service).validate(token);
+		}
+
+		@Test
+		void shouldCallDecryptAttributes() {
+			validate();
+
+			verify(service).decryptAttributes(token);
+		}
+
+		@Test
+		void shouldCallBuildTokenAttributes() {
+			validate();
+
+			verify(service).buildTokenAttributes(TOKEN_ATTRIBUTES_MAP, token);
+		}
+
+		@Test
+		void shouldReturnResult() {
+			var result = validate();
+
+			assertThat(result).containsExactly(TOKEN_ATTRIBUTE);
+		}
+
+		private Set<TokenAttribute> validate() {
+			return service.validate(token);
+		}
+	}
+
+	@Nested
+	class TestValidateToken {
+
+		@Mock
+		private Response token;
+
+		@Nested
+		class TestValidateSuccessfully {
+
+			@Mock
+			private Signature signature;
+
+			@BeforeEach
+			void init() {
+				when(token.getSignature()).thenReturn(signature);
+				doReturn(true).when(service).validateSignature(any());
+			}
+
+			@Test
+			void shouldCallValidateSignatureProfile() {
+				validateToken();
+
+				verify(service).validateSignatureProfile(signature);
+			}
+
+			@Test
+			void shouldValidateToken() {
+				validateToken();
+
+				Assertions.assertDoesNotThrow(TestValidateToken.this::validateToken);
+			}
+		}
+
+		@Nested
+		class TestValidationFailed {
+
+			@Mock
+			private Signature signature;
+
+			@Test
+			void shouldThrowWhenMissingSignature() {
+				assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken);
+			}
+
+			@Test
+			void shouldThrowWhenInvalidSignatureProfile() {
+				when(token.getSignature()).thenReturn(signature);
+				when(service.validateSignature(any())).thenReturn(false);
+
+				assertThrows(TokenVerificationException.class, TestValidateToken.this::validateToken);
+			}
+		}
+
+		private void validateToken() {
+			service.validateToken(token);
+		}
+	}
+
+	@Nested
+	class TestValidateSignatureProfile {
+
+		@Mock
+		private Signature signature;
+
+		@Test
+		@SneakyThrows
+		void shouldCallProfileValidator() {
+			service.validateSignatureProfile(signature);
+
+			verify(profileValidator).validate(signature);
+		}
+	}
+
+	@Nested
+	class TestValidateSignature {
+
+		@Mock
+		private Signature signature;
+
+		@Test
+		@SneakyThrows
+		void shouldCallSignatureTrustEngine() {
+			service.validateSignature(signature);
+
+			verify(signatureTrustEngine).validate(signature, verificationCriteria);
+		}
+	}
+
+	@Nested
+	class TestDecryptAttributes {
+
+		@Mock
+		private Response token;
+		@Mock
+		private EncryptedAssertion encryptedAssertion;
+		@Mock
+		private Assertion assertion;
+		@Mock
+		private AttributeStatement attributeStatement;
+		@Mock
+		private Attribute attribute;
+
+		@BeforeEach
+		void init() {
+			doReturn(assertion).when(service).decryptAssertion(any());
+			doReturn(Optional.of(attributeStatement)).when(service).getAttributeStatement(any());
+			doReturn(TokenAttributeTestFactory.VALUE).when(service).getAttributeValues(attribute);
+			when(token.getEncryptedAssertions()).thenReturn(List.of(encryptedAssertion));
+			when(attributeStatement.getAttributes()).thenReturn(List.of(attribute));
+			when(attribute.getName()).thenReturn(TokenAttributeTestFactory.NAME);
+		}
+
+		@Test
+		void shouldCallDecryptAssertion() {
+			decryptAttributes();
+
+			verify(service).decryptAssertion(encryptedAssertion);
+		}
+
+		@Test
+		void shouldCallGetAttributeStatement() {
+			decryptAttributes();
+
+			verify(service).getAttributeStatement(assertion);
+		}
+
+		@Test
+		void shouldCallGetAttributeValues() {
+			decryptAttributes();
+
+			verify(service).getAttributeValues(attribute);
+		}
+
+		@Test
+		void shouldReturnAttributes() {
+			var result = decryptAttributes();
+
+			assertThat(result).isEqualTo(TokenAttributeTestFactory.asMap());
+		}
+
+		private Map<String, String> decryptAttributes() {
+			return service.decryptAttributes(token);
+		}
+	}
+
+	@Nested
+	class TestDecryptAssertion {
+
+		@Mock
+		private EncryptedAssertion encryptedAssertion;
+		@Mock
+		private Assertion assertion;
+
+		@SneakyThrows
+		@BeforeEach
+		void init() {
+			doReturn(assertion).when(decrypter).decrypt(any(EncryptedAssertion.class));
+		}
+
+		@Test
+		@SneakyThrows
+		void shouldDecryptAssertion() {
+			service.decryptAssertion(encryptedAssertion);
+
+			verify(decrypter).decrypt(encryptedAssertion);
+		}
+
+		@Test
+		void shouldReturnResult() {
+			var result = service.decryptAssertion(encryptedAssertion);
+
+			assertThat(result).isEqualTo(assertion);
+		}
+	}
+
+	@Nested
+	class TestGetAttributeStatement {
+
+		@Mock
+		private Assertion assertion;
+		@Mock
+		private AttributeStatement attributeStatement;
+		@Mock
+		private Statement statement;
+
+		@Test
+		void shouldReturnAttributeStatement() {
+			when(assertion.getStatements()).thenReturn(List.of(attributeStatement));
+
+			var result = service.getAttributeStatement(assertion);
+
+			assertThat(result).contains(attributeStatement);
+		}
+
+		@Test
+		void shouldReturnEmptyOptional() {
+			when(assertion.getStatements()).thenReturn(List.of(statement));
+
+			var result = service.getAttributeStatement(assertion);
+
+			assertThat(result).isEmpty();
+		}
+	}
+
+	@Nested
+	class TestGetAttributeValues {
+
+		private static final String VALUE1 = LoremIpsum.getInstance().getWords(1);
+		private static final String VALUE2 = LoremIpsum.getInstance().getWords(1);
+
+		@Mock
+		private Attribute attribute;
+		@Mock
+		private XMLObject attributeValue1;
+		@Mock
+		private XMLObject attributeValue2;
+
+		@BeforeEach
+		void init() {
+			doReturn(VALUE1).when(service).getAttributeValue(attributeValue1);
+		}
+
+		@Test
+		void shouldCallGetAttributeValue() {
+			when(attribute.getAttributeValues()).thenReturn(List.of(attributeValue1));
+
+			service.getAttributeValues(attribute);
+
+			verify(service).getAttributeValue(attributeValue1);
+		}
+
+		@Test
+		void shouldReturnAttributeValues() {
+			doReturn(VALUE2).when(service).getAttributeValue(attributeValue2);
+			when(attribute.getAttributeValues()).thenReturn(List.of(attributeValue1, attributeValue2));
+
+			var result = service.getAttributeValues(attribute);
+
+			assertThat(result).isEqualTo(VALUE1 + ";" + VALUE2);
+		}
+	}
+
+	@Nested
+	class TestGetAttributeValue {
+
+		@Test
+		void shouldReturnEmptyString() {
+			var result = service.getAttributeValue(null);
+
+			assertThat(result).isEmpty();
+		}
+
+		@Test
+		void shouldReturnWhenXSString() {
+			XMLObject attrValue = when(mock(XSString.class).getValue()).thenReturn(TokenAttributeTestFactory.VALUE).getMock();
+
+			var result = service.getAttributeValue(attrValue);
+
+			assertThat(result).isEqualTo(TokenAttributeTestFactory.VALUE);
+		}
+
+		@Test
+		void shouldReturnWhenXSAny() {
+			XMLObject attrValue = when(mock(XSAny.class).getTextContent()).thenReturn(TokenAttributeTestFactory.VALUE).getMock();
+
+			var result = service.getAttributeValue(attrValue);
+
+			assertThat(result).isEqualTo(TokenAttributeTestFactory.VALUE);
+		}
+
+		@Test
+		void shouldReturnWhenXSInteger() {
+			var value = new Random().nextInt();
+			XMLObject attrValue = when(mock(XSInteger.class).getValue()).thenReturn(value).getMock();
+
+			var result = service.getAttributeValue(attrValue);
+
+			assertThat(result).isEqualTo(String.valueOf(value));
+		}
+
+		@Test
+		void shouldReturnWhenXSBoolean() {
+			var value = String.valueOf(new Random().nextBoolean());
+			XSBooleanValue booleanValue = when(mock(XSBooleanValue.class).toString()).thenReturn(value).getMock();
+			XMLObject attrValue = when(mock(XSBoolean.class).getValue()).thenReturn(booleanValue).getMock();
+
+			var result = service.getAttributeValue(attrValue);
+
+			assertThat(result).isEqualTo(value);
+		}
+	}
+
+	@Nested
+	class TestBuildTokenAttributes {
+
+		private static final Map.Entry<String, String> ADJUSTED_TOKEN_ATTRIBUTES_ENTRY = Map.entry("key", "value");
+		private static final Map<String, String> ADJUSTED_TOKEN_ATTRIBUTES_MAP = Map.ofEntries(ADJUSTED_TOKEN_ATTRIBUTES_ENTRY);
+		private static final TokenAttribute TOKEN_ATTRIBUTE = TokenAttributeTestFactory.create();
+
+		@Mock
+		private Response token;
+
+		@BeforeEach
+		void init() {
+			doReturn(ADJUSTED_TOKEN_ATTRIBUTES_MAP).when(service).adjustPostfachIdAttribute(any(), any());
+			doReturn(TOKEN_ATTRIBUTE).when(service).buildTokenAttribute(any());
+		}
+
+		@Test
+		void shouldCallAdjustPostfachIdAttribute() {
+			buildTokenAttributes();
+
+			verify(service).adjustPostfachIdAttribute(TokenAttributeTestFactory.asMap(), token);
+		}
+
+		@Test
+		void shouldCallBuildTokenAttribute() {
+			buildTokenAttributes();
+
+			verify(service).buildTokenAttribute(ADJUSTED_TOKEN_ATTRIBUTES_ENTRY);
+		}
+
+		@Test
+		void shouldReturnTokenAttributes() {
+			var result = buildTokenAttributes();
+
+			assertThat(result).containsExactly(TOKEN_ATTRIBUTE);
+		}
+
+		private Set<TokenAttribute> buildTokenAttributes() {
+			return service.buildTokenAttributes(TokenAttributeTestFactory.asMap(), token);
+		}
+	}
+
+	@Nested
+	class TestAdjustPostfachIdAttribute {
+
+		private static final String POSTFACH_ID = LoremIpsum.getInstance().getWords(1);
+
+		@Mock
+		private Response token;
+
+		@Test
+		void shouldSetPostfachId() {
+			when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(true);
+			when(token.getID()).thenReturn(POSTFACH_ID);
+
+			var result = adjustPostfachIdAttribute();
+
+			assertThat(result).containsEntry(TokenAttribute.POSTFACH_ID_KEY, POSTFACH_ID);
+		}
+
+		@Test
+		void shouldRewritePostfachId() {
+			when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(true);
+			var attributeMap = Map.of(TokenAttribute.POSTFACH_ID_KEY, LoremIpsum.getInstance().getWords(1));
+			when(token.getID()).thenReturn(POSTFACH_ID);
+
+			var result = service.adjustPostfachIdAttribute(attributeMap, token);
+
+			assertThat(result).containsEntry(TokenAttribute.POSTFACH_ID_KEY, POSTFACH_ID);
+		}
+
+		@Test
+		void shouldReturnUnchangedMap() {
+			when(tokenValidationProperty.isUseIdAsPostfachId()).thenReturn(false);
+			var attributeMap = TokenAttributeTestFactory.asMap();
+
+			var result = service.adjustPostfachIdAttribute(attributeMap, token);
+
+			assertThat(result).isEqualTo(attributeMap);
+		}
+
+		private Map<String, String> adjustPostfachIdAttribute() {
+			return service.adjustPostfachIdAttribute(TokenAttributeTestFactory.asMap(), token);
+		}
+	}
+
+	@Nested
+	class TestBuildTokenAttribute {
+
+		@Test
+		void shouldBuildTokenAttribute() {
+			var result = service.buildTokenAttribute(Map.entry(TokenAttributeTestFactory.NAME, TokenAttributeTestFactory.VALUE));
+
+			assertThat(result).isEqualTo(TokenAttributeTestFactory.create());
+		}
+	}
+}
\ No newline at end of file
-- 
GitLab