From 3a4e0312341f37071f33c239ddf9fbf42c3eb809 Mon Sep 17 00:00:00 2001
From: OZGCloud <ozgcloud@mgm-tp.com>
Date: Mon, 2 Dec 2024 17:52:11 +0100
Subject: [PATCH] OZG-7092 [test] add tests

---
 .../token/saml/SamlTrustEngineFactory.java    |   4 +-
 .../saml/SamlTrustEngineFactoryTest.java      | 540 +++++++++++++++++-
 2 files changed, 528 insertions(+), 16 deletions(-)

diff --git a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java
index b1b3de4..fd9567a 100644
--- a/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java
+++ b/token-checker-server/src/main/java/de/ozgcloud/token/saml/SamlTrustEngineFactory.java
@@ -81,8 +81,8 @@ class SamlTrustEngineFactory {
 		return new CollectionCredentialResolver(credentials);
 	}
 
-	Stream<Saml2X509Credential> getCertificatesFromMetadata(Resource metadataResource) {
-		try (var metadataInputStream = metadataResource.getInputStream()) {
+	Stream<Saml2X509Credential> getCertificatesFromMetadata(Resource metadata) {
+		try (var metadataInputStream = metadata.getInputStream()) {
 			return findEntityDescriptor(getXmlObject(metadataInputStream))
 					.map(this::getVerificationCertificates)
 					.orElseThrow(() -> new TechnicalException("No IDPSSO Descriptors found"));
diff --git a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java
index 09e569c..5260ac4 100644
--- a/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java
+++ b/token-checker-server/src/test/java/de/ozgcloud/token/saml/SamlTrustEngineFactoryTest.java
@@ -24,29 +24,56 @@
 package de.ozgcloud.token.saml;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
+import java.io.InputStream;
+import java.security.cert.X509Certificate;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
 import java.util.stream.Stream;
 
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Spy;
+import org.opensaml.core.xml.XMLObject;
 import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
+import org.opensaml.core.xml.io.Unmarshaller;
+import org.opensaml.core.xml.io.UnmarshallerFactory;
+import org.opensaml.saml.common.xml.SAMLConstants;
+import org.opensaml.saml.saml2.metadata.EntitiesDescriptor;
+import org.opensaml.saml.saml2.metadata.EntityDescriptor;
+import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
+import org.opensaml.saml.saml2.metadata.KeyDescriptor;
+import org.opensaml.security.credential.UsageType;
 import org.opensaml.security.credential.impl.CollectionCredentialResolver;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.security.x509.X509Credential;
+import org.opensaml.xmlsec.keyinfo.KeyInfoSupport;
+import org.opensaml.xmlsec.signature.KeyInfo;
 import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine;
 import org.springframework.core.io.Resource;
 import org.springframework.security.saml2.core.Saml2X509Credential;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
 
 import de.ozgcloud.common.errorhandling.TechnicalException;
 import de.ozgcloud.token.TokenValidationProperties.TokenValidationProperty;
+import lombok.SneakyThrows;
 import net.shibboleth.utilities.java.support.xml.ParserPool;
 
 class SamlTrustEngineFactoryTest {
 
+	private static final String IDP_ENTITY_ID = UUID.randomUUID().toString();
+
 	@Spy
 	@InjectMocks
 	private SamlTrustEngineFactory factory;
@@ -101,36 +128,521 @@ class SamlTrustEngineFactoryTest {
 		@Mock
 		private Resource metadata;
 
+		@Nested
+		class TestBuildSuccessfully {
+
+			@Mock
+			private X509Credential x509Credential;
+
+			@BeforeEach
+			void init() {
+				when(tokenValidationProperty.getMetadata()).thenReturn(metadata);
+				when(tokenValidationProperty.getIdpEntityId()).thenReturn(IDP_ENTITY_ID);
+				doReturn(Stream.of(credential)).when(factory).getCertificatesFromMetadata(any());
+				doReturn(x509Credential).when(factory).buildBasicX509Credential(any(), any());
+			}
+
+			@Test
+			void shouldCallGetCertificatesFromMetadata() {
+				buildCredentialResolver();
+
+				verify(factory).getCertificatesFromMetadata(metadata);
+			}
+
+			@Test
+			void shouldCallBuildBasicX509Credential() {
+				buildCredentialResolver();
+
+				verify(factory).buildBasicX509Credential(credential, IDP_ENTITY_ID);
+			}
+
+			@Test
+			void shouldBuildCredentialResolver() {
+				var result = buildCredentialResolver();
+
+				assertThat(result.getCollection()).containsExactly(x509Credential);
+			}
+		}
+
+		@Nested
+		class TestThrowException {
+
+			@Test
+			void shouldThrowExceptionWhenNoCertificates() {
+				doReturn(Stream.empty()).when(factory).getCertificatesFromMetadata(any());
+
+				assertThrows(TechnicalException.class, TestBuildCredentialResolver.this::buildCredentialResolver);
+			}
+		}
+
+		private CollectionCredentialResolver buildCredentialResolver() {
+			return factory.buildCredentialResolver(tokenValidationProperty);
+		}
+	}
+
+	@Nested
+	class TestGetCertificatesFromMetadata {
+
+		@Mock
+		private Resource metadata;
+		@Mock
+		private InputStream inputStream;
+		@Mock
+		private XMLObject xmlObject;
+		@Mock
+		private Saml2X509Credential credential;
+		@Mock
+		private EntityDescriptor entityDescriptor;
+
+		@BeforeEach
+		@SneakyThrows
+		void init() {
+			when(metadata.getInputStream()).thenReturn(inputStream);
+			doReturn(xmlObject).when(factory).getXmlObject(any());
+		}
+
+		@Nested
+		class TestSuccessfully {
+
+			@BeforeEach
+			void init() {
+				doReturn(Optional.of(entityDescriptor)).when(factory).findEntityDescriptor(any());
+				doReturn(Stream.of(credential)).when(factory).getVerificationCertificates(any());
+			}
+
+			@Test
+			void shouldCallGetXmlObject() {
+				getCertificatesFromMetadata();
+
+				verify(factory).getXmlObject(inputStream);
+			}
+
+			@Test
+			void shouldCallFindEntityDescriptor() {
+				getCertificatesFromMetadata();
+
+				verify(factory).findEntityDescriptor(xmlObject);
+			}
+
+			@Test
+			void shouldGetVerificationCertificates() {
+				getCertificatesFromMetadata();
+
+				verify(factory).getVerificationCertificates(entityDescriptor);
+			}
+
+			@Test
+			void shouldReturnResult() {
+				var result = getCertificatesFromMetadata();
+
+				assertThat(result).containsExactly(credential);
+			}
+		}
+
+		@Nested
+		class TestWithException {
+
+			@Test
+			void shouldThrowException() {
+				doReturn(Optional.empty()).when(factory).findEntityDescriptor(any());
+
+				assertThrows(TechnicalException.class, TestGetCertificatesFromMetadata.this::getCertificatesFromMetadata);
+			}
+		}
+
+		private List<Saml2X509Credential> getCertificatesFromMetadata() {
+			return factory.getCertificatesFromMetadata(metadata).toList();
+		}
+
+	}
+
+	@Nested
+	class TestGetXmlObject {
+
+		@Mock
+		private InputStream inputStream;
+		@Mock
+		private Unmarshaller unmarshaller;
+		@Mock
+		private XMLObject xmlObject;
+		@Mock
+		private Document document;
+		@Mock
+		private Element element;
+
 		@BeforeEach
+		@SneakyThrows
 		void init() {
-			when(tokenValidationProperty.getMetadata()).thenReturn(metadata);
+			when(parserPool.parse(any(InputStream.class))).thenReturn(document);
+			when(document.getDocumentElement()).thenReturn(element);
+			doReturn(unmarshaller).when(factory).getUnmarshaller(any());
+			when(unmarshaller.unmarshall(element)).thenReturn(xmlObject);
 		}
 
 		@Test
-		void shouldCallGetCertificatesFromMetadata() {
-			when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.of(credential));
+		@SneakyThrows
+		void shouldCallParse() {
+			getXmlObject();
 
-			factory.buildCredentialResolver(tokenValidationProperty);
+			verify(parserPool).parse(inputStream);
+		}
+
+		@Test
+		void shouldCallGetUnmarshaller() {
+			getXmlObject();
 
-			verify(factory).getCertificatesFromMetadata(metadata);
+			verify(factory).getUnmarshaller(element);
 		}
 
 		@Test
-		void shouldThrowExceptionWhenNoCertificates() {
-			when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.empty());
+		@SneakyThrows
+		void shouldCallUnmarshall() {
+			getXmlObject();
 
-			assertThatThrownBy(() -> factory.buildCredentialResolver(tokenValidationProperty)).isInstanceOf(TechnicalException.class)
-					.hasMessage("Metadata response is missing verification certificates, necessary for verifying SAML assertions");
+			verify(unmarshaller).unmarshall(element);
 		}
 
 		@Test
-		void shouldBuildCredentialResolver() {
-			when(factory.getCertificatesFromMetadata(any())).thenReturn(Stream.of("key"));
-			doReturn(new CollectionCredentialResolver(null)).when(factory).buildCredentialResolver(any());
+		void shouldReturnResult() {
+			var result = getXmlObject();
+
+			assertThat(result).isEqualTo(xmlObject);
+		}
+
+		private XMLObject getXmlObject() {
+			return factory.getXmlObject(inputStream);
+		}
+	}
+
+	@Nested
+	class TestGetUnmarshaller {
+
+		@Mock
+		private Element element;
+		@Mock
+		private UnmarshallerFactory unmarshallerFactory;
+		@Mock
+		private Unmarshaller unmarshaller;
+
+		@BeforeEach
+		void init() {
+			when(registry.getUnmarshallerFactory()).thenReturn(unmarshallerFactory);
+		}
+
+		@Nested
+		class TestSuccessfully {
+
+			@BeforeEach
+			void init() {
+				when(unmarshallerFactory.getUnmarshaller(any(Element.class))).thenReturn(unmarshaller);
+			}
+
+			@Test
+			void shouldCallGetUnmarshallerFactory() {
+				getUnmarshaller();
+
+				verify(registry).getUnmarshallerFactory();
+			}
+
+			@Test
+			void shouldCallGetUnmarshaller() {
+				getUnmarshaller();
+
+				verify(unmarshallerFactory).getUnmarshaller(element);
+			}
 
-			var result = factory.buildCredentialResolver(tokenValidationProperty);
+			@Test
+			void shouldReturnResult() {
+				var result = getUnmarshaller();
+
+				assertThat(result).isEqualTo(unmarshaller);
+			}
+		}
+
+		@Nested
+		class TestNoUnmarshaller {
+
+			@Test
+			void shouldThrowException() {
+				assertThrows(TechnicalException.class, TestGetUnmarshaller.this::getUnmarshaller);
+			}
+		}
+
+		private Unmarshaller getUnmarshaller() {
+			return factory.getUnmarshaller(element);
+		}
+	}
+
+	@Nested
+	class TestFindEntityDescriptor {
+
+		@Mock
+		private XMLObject xmlObject;
+		@Mock
+		private EntityDescriptor entityDescriptor;
+		@Mock
+		private IDPSSODescriptor descriptor;
+
+		@BeforeEach
+		void init() {
+			when(factory.extractEntityDescriptor(any())).thenReturn(Optional.of(entityDescriptor));
+		}
+
+		@Test
+		void shouldCallExtractEntityDescriptor() {
+			findEntityDescriptor();
+
+			verify(factory).extractEntityDescriptor(xmlObject);
+		}
+
+		@Test
+		void shouldFilterDescriptor() {
+			when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(descriptor);
+
+			findEntityDescriptor();
+
+			verify(entityDescriptor).getIDPSSODescriptor(SAMLConstants.SAML20P_NS);
+		}
+
+		@Test
+		void shouldReturnResult() {
+			when(entityDescriptor.getIDPSSODescriptor(anyString())).thenReturn(descriptor);
+
+			var result = findEntityDescriptor();
+
+			assertThat(result).contains(entityDescriptor);
+		}
+
+		private Optional<EntityDescriptor> findEntityDescriptor() {
+			return factory.findEntityDescriptor(xmlObject);
+		}
+	}
+
+	@Nested
+	class TestExtractEntityDescriptor {
+
+		@Mock
+		private XMLObject xmlObject;
+
+		@Test
+		void shouldReturnEmpty() {
+			var result = factory.extractEntityDescriptor(xmlObject);
+
+			assertThat(result).isEmpty();
+		}
+
+		@Nested
+		class TestEntityDescriptor {
+
+			@Mock
+			private EntityDescriptor entityDescriptor;
+
+			@Test
+			void shouldReturnEntityDescriptor() {
+				var result = factory.extractEntityDescriptor(entityDescriptor);
+
+				assertThat(result).contains(entityDescriptor);
+			}
+		}
+
+		@Nested
+		class TestEntitiesDescriptor {
+
+			@Mock
+			private EntitiesDescriptor entitiesDescriptor;
+			@Mock
+			private EntityDescriptor entityDescriptor;
+
+			@BeforeEach
+			void init() {
+				when(entitiesDescriptor.getEntityDescriptors()).thenReturn(List.of(entityDescriptor));
+			}
+
+			@Test
+			void shouldReturnEntityDescriptor() {
+				var result = factory.extractEntityDescriptor(entitiesDescriptor);
+
+				assertThat(result).contains(entityDescriptor);
+			}
+		}
+	}
+
+	@Nested
+	class TestGetVerificationCertificates {
+
+		@Mock
+		private EntityDescriptor descriptor;
+
+		@Nested
+		class TestSuccessfully {
+
+			@Mock
+			private IDPSSODescriptor idpSsoDescriptor;
+			@Mock
+			private KeyDescriptor keyDescriptor;
+			@Mock
+			private X509Certificate certificate;
+
+			@BeforeEach
+			void init() {
+				when(descriptor.getIDPSSODescriptor(anyString())).thenReturn(idpSsoDescriptor);
+				when(idpSsoDescriptor.getKeyDescriptors()).thenReturn(List.of(keyDescriptor));
+				doReturn(true).when(factory).isSignatureKey(any());
+				doReturn(Stream.of(certificate)).when(factory).getCertificates(any());
+			}
+
+			@Test
+			void shouldCallGetKeyDescriptors() {
+				getVerificationCertificates();
+
+				verify(idpSsoDescriptor).getKeyDescriptors();
+			}
+
+			@Test
+			void shouldCallIsSignatureKey() {
+				getVerificationCertificates();
+
+				verify(factory).isSignatureKey(keyDescriptor);
+			}
+
+			@Test
+			void shouldCallGetCertificates() {
+				getVerificationCertificates();
+
+				verify(factory).getCertificates(keyDescriptor);
+			}
+
+			@Test
+			void shouldCallVerification() {
+				try (var credentialMock = mockStatic(Saml2X509Credential.class)) {
+					getVerificationCertificates();
+
+					credentialMock.verify(() -> Saml2X509Credential.verification(certificate));
+				}
+			}
+		}
+
+		@Nested
+		class TestWithException {
+
+			@Test
+			void shouldThrowException() {
+				assertThrows(TechnicalException.class, TestGetVerificationCertificates.this::getVerificationCertificates);
+			}
+		}
+
+		private List<Saml2X509Credential> getVerificationCertificates() {
+			return factory.getVerificationCertificates(descriptor).toList();
+		}
+	}
+
+	@Nested
+	class TestIsSignatureKey {
+
+		@Mock
+		private KeyDescriptor keyDescriptor;
+
+		@Test
+		void shouldReturnTrue() {
+			when(keyDescriptor.getUse()).thenReturn(UsageType.SIGNING);
+
+			var result = isSignatureKey();
+
+			assertThat(result).isTrue();
+		}
+
+		@DisplayName("should return false")
+		@ParameterizedTest(name = "when keyDescriptor use is {0}")
+		@EnumSource(value = UsageType.class, names = { "SIGNING" }, mode = EnumSource.Mode.EXCLUDE)
+		void shouldReturnFalse(UsageType use) {
+			when(keyDescriptor.getUse()).thenReturn(use);
+
+			var result = isSignatureKey();
+
+			assertThat(result).isFalse();
+		}
+
+		private boolean isSignatureKey() {
+			return factory.isSignatureKey(keyDescriptor);
+		}
+	}
+
+	@Nested
+	class TestGetCertificates {
+
+		@Mock
+		private KeyDescriptor keyDescriptor;
+		@Mock
+		private KeyInfo keyInfo;
+		@Mock
+		private X509Certificate certificate;
+
+		@BeforeEach
+		void init() {
+			when(keyDescriptor.getKeyInfo()).thenReturn(keyInfo);
+		}
+
+		@Test
+		void shouldCallGetCertificates() {
+			try (var keyInfoSupportMock = mockStatic(KeyInfoSupport.class)) {
+				getCertificates();
+
+				keyInfoSupportMock.verify(() -> KeyInfoSupport.getCertificates(keyInfo));
+			}
+		}
+
+		@Test
+		void shouldReturnResult() {
+			try (var keyInfoSupportMock = mockStatic(KeyInfoSupport.class)) {
+				keyInfoSupportMock.when(() -> KeyInfoSupport.getCertificates(any(KeyInfo.class))).thenReturn(List.of(certificate));
+
+				var result = getCertificates();
+
+				assertThat(result).containsExactly(certificate);
+			}
+		}
+
+		private List<X509Certificate> getCertificates() {
+			return factory.getCertificates(keyDescriptor).toList();
+		}
+	}
+
+	@Nested
+	class TestBuildBasicX509Credential {
+
+		@Mock
+		private Saml2X509Credential samlKey;
+		@Mock
+		private X509Certificate certificate;
+
+		@BeforeEach
+		void init() {
+			when(samlKey.getCertificate()).thenReturn(certificate);
+		}
+
+		@Test
+		void shouldSetEntityCertificate() {
+			var result = buildBasicX509Credential();
+
+			assertThat(result.getEntityCertificate()).isEqualTo(certificate);
+		}
+
+		@Test
+		void shouldSetUsageType() {
+			var result = buildBasicX509Credential();
+
+			assertThat(result.getUsageType()).isEqualTo(UsageType.SIGNING);
+		}
+
+		@Test
+		void shouldSetEntityId() {
+			var result = buildBasicX509Credential();
+
+			assertThat(result.getEntityId()).isEqualTo(IDP_ENTITY_ID);
+		}
 
-			assertThat(result).isNotNull();
+		private BasicX509Credential buildBasicX509Credential() {
+			return (BasicX509Credential) factory.buildBasicX509Credential(samlKey, IDP_ENTITY_ID);
 		}
 	}
 }
\ No newline at end of file
-- 
GitLab