From 072c3c31303097da4e2ff48ae461d663f4ce462f Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Thu, 17 May 2018 16:19:27 +0000 Subject: [PATCH] Merged in SHIBUI-468 (pull request #72) SHIBUI-468 * [SHIBUI-440] Unit test additions WIP * [SHIBUI-440] Added simple tests to ensure that file creation and deletion happens as expected. * [SHIBUI-440] Heavily refactored JPAEntityServiceImpl.getAttributeListFromEntityRepresentation. Added more tests in an attempt to increase coverage. Removed a couple default constructors. Added a helper util for counting attributes from a RelyingPartyOverridesRepresentation. This could maybe use a better name. * [SHIBUI-440] Added a unit test to check for the exception that we always throw here. * [SHIBUI-440] Added a WIP unit test. There are a few that still need to be added to this class. * [SHIBUI-468] Added tests for XML-related POSTs/GETs of EntityDescriptors, * [SHIBUI-468] Added simple auth test. --- .../admin/ui/service/EntityService.java | 1 + .../JPAEntityDescriptorServiceImpl.java | 3 - .../ui/service/JPAEntityServiceImpl.java | 72 ++--- ...faultAuthenticationIntegrationTests.groovy | 36 +++ .../EntityDescriptorControllerTests.groovy | 263 +++++++++++++++++- ...yDescriptorFilesScheduledTasksTests.groovy | 143 ++++++++++ ...JPAEntityDescriptorServiceImplTests.groovy | 128 ++++++++- .../service/JPAEntityServiceImplTests.groovy | 92 ++++++ .../service/JPAFilterServiceImplTests.groovy | 30 +- .../admin/ui/util/TestHelpers.groovy | 25 ++ 10 files changed, 712 insertions(+), 81 deletions(-) create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy create mode 100644 backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java index fbb85f6d2..5a4a65a25 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/EntityService.java @@ -11,6 +11,7 @@ */ public interface EntityService { List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation); + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute getAttributeFromAttributeReleaseList(List attributeReleaseList); List getAttributeListFromAttributeReleaseList(List attributeReleaseList); List getAttributeListFromRelyingPartyOverridesRepresentation(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index 74a091641..9f3da24d3 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -71,9 +71,6 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private EntityService entityService; - public JPAEntityDescriptorServiceImpl() { - } - public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService) { this.openSamlObjects = openSamlObjects; this.entityService = entityService; diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java index ff5c3981a..263f0c01e 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImpl.java @@ -22,8 +22,6 @@ public class JPAEntityServiceImpl implements EntityService { @Autowired private AttributeUtility attributeUtility; - public JPAEntityServiceImpl() {} - public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects) { this.openSamlObjects = openSamlObjects; } @@ -32,55 +30,40 @@ public JPAEntityServiceImpl(OpenSamlObjects openSamlObjects) { public List getAttributeListFromEntityRepresentation(EntityDescriptorRepresentation entityDescriptorRepresentation) { List list = new ArrayList<>(); if (entityDescriptorRepresentation.getRelyingPartyOverrides() != null) { - // Let's do the overrides - RelyingPartyOverridesRepresentation overridesRepresentation = entityDescriptorRepresentation.getRelyingPartyOverrides(); - if (overridesRepresentation.isSignAssertion()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_ASSERTIONS, MDDCConstants.SIGN_ASSERTIONS_FN, true)); - } - if (overridesRepresentation.isDontSignResponse()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.SIGN_RESPONSES, MDDCConstants.SIGN_RESPONSES_FN, false)); - } - if (overridesRepresentation.isTurnOffEncryption()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.ENCRYPT_ASSERTIONS, MDDCConstants.ENCRYPT_ASSERTIONS_FN, false)); - } - if (overridesRepresentation.isUseSha()) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.SECURITY_CONFIGURATION, MDDCConstants.SECURITY_CONFIGURATION_FN, "shibboleth.SecurityConfiguration.SHA1")); - } - if (overridesRepresentation.isIgnoreAuthenticationMethod()) { - // this is actually going to be wrong, but it will work for the time being. this should be a bitmask value that we calculate - // TODO: fix - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DISALLOWED_FEATURES, MDDCConstants.DISALLOWED_FEATURES_FN, "0x1")); - } - if (overridesRepresentation.isOmitNotBefore()) { - list.add(attributeUtility.createAttributeWithBooleanValue(MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE, MDDCConstants.INCLUDE_CONDITIONS_NOT_BEFORE_FN, false)); - } - if (overridesRepresentation.getResponderId() != null && !"".equals(overridesRepresentation.getResponderId())) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.RESPONDER_ID, MDDCConstants.RESPONDER_ID_FN, overridesRepresentation.getResponderId())); - } - if (overridesRepresentation.getNameIdFormats() != null && overridesRepresentation.getNameIdFormats().size() > 0) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.NAME_ID_FORMAT_PRECEDENCE, MDDCConstants.NAME_ID_FORMAT_PRECEDENCE_FN, overridesRepresentation.getNameIdFormats())); - } - if (overridesRepresentation.getAuthenticationMethods() != null && overridesRepresentation.getAuthenticationMethods().size() > 0) { - list.add(attributeUtility.createAttributeWithArbitraryValues(MDDCConstants.DEFAULT_AUTHENTICATION_METHODS, MDDCConstants.DEFAULT_AUTHENTICATION_METHODS_FN, overridesRepresentation.getAuthenticationMethods())); - } + getAttributeListFromRelyingPartyOverridesRepresentation(entityDescriptorRepresentation.getRelyingPartyOverrides()).forEach(attribute -> + list.add((edu.internet2.tier.shibboleth.admin.ui.domain.Attribute) attribute) + ); } // let's map the attribute release if (entityDescriptorRepresentation.getAttributeRelease() != null && entityDescriptorRepresentation.getAttributeRelease().size() > 0) { - edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((AttributeBuilder) openSamlObjects.getBuilderFactory().getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)).buildObject(); - list.add(attribute); - - attribute.setName(MDDCConstants.RELEASE_ATTRIBUTES); - - for (String attributeRelease : entityDescriptorRepresentation.getAttributeRelease()) { - XSString xsString = (XSString) openSamlObjects.getBuilderFactory().getBuilder(XSString.TYPE_NAME).buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); - xsString.setValue(attributeRelease); - attribute.getAttributeValues().add(xsString); - } + list.add(getAttributeFromAttributeReleaseList(entityDescriptorRepresentation.getAttributeRelease())); } + return (List)(List)list; } + @Override + public edu.internet2.tier.shibboleth.admin.ui.domain.Attribute getAttributeFromAttributeReleaseList(List attributeReleaseList) { + edu.internet2.tier.shibboleth.admin.ui.domain.Attribute attribute = ((AttributeBuilder) openSamlObjects + .getBuilderFactory() + .getBuilder(edu.internet2.tier.shibboleth.admin.ui.domain.Attribute.DEFAULT_ELEMENT_NAME)) + .buildObject(); + + attribute.setName(MDDCConstants.RELEASE_ATTRIBUTES); + + attributeReleaseList.forEach(attributeRelease -> { + XSString xsString = (XSString) openSamlObjects + .getBuilderFactory() + .getBuilder(XSString.TYPE_NAME) + .buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + xsString.setValue(attributeRelease); + attribute.getAttributeValues().add(xsString); + }); + + return attribute; + } + @Override public List getAttributeListFromAttributeReleaseList(List attributeReleaseList) { List attributeList = new ArrayList<>(); @@ -130,7 +113,4 @@ public List getAttributeListFromRelyingPartyOverridesRepresentation(R return (List)(List)list; } - - - } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy new file mode 100644 index 000000000..43b6c850e --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/DefaultAuthenticationIntegrationTests.groovy @@ -0,0 +1,36 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.util.DefaultUriBuilderFactory +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("default") +class DefaultAuthenticationIntegrationTests extends Specification { + + @Autowired + private WebTestClient webClient + + def setup() { + this.webClient.webClient.uriBuilderFactory.encodingMode = DefaultUriBuilderFactory.EncodingMode.NONE + } + + def "When auth is enabled and an unauth'd request is made, a 302 is returned which points at login"() { + when: + def result = this.webClient + .get() + .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") + .exchange() + + then: + result + .expectStatus().isEqualTo(302) + .expectHeader().valueMatches("Location", "http://localhost:\\d*/login") + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index 4496f44ec..739243d24 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -6,29 +6,32 @@ import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import groovy.json.JsonOutput import groovy.json.JsonSlurper import org.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.client.RestTemplate import spock.lang.Specification import spock.lang.Subject import java.time.LocalDateTime import static org.hamcrest.CoreMatchers.containsString -import static org.springframework.http.MediaType.APPLICATION_JSON_UTF8 -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put +import static org.springframework.http.MediaType.* +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* class EntityDescriptorControllerTests extends Specification { - def generator + RandomGenerator randomGenerator + TestObjectGenerator generator + def mapper def service def entityDescriptorRepository = Mock(EntityDescriptorRepository) + def mockRestTemplate = Mock(RestTemplate) def openSamlObjects = new OpenSamlObjects().with { init() @@ -42,19 +45,19 @@ class EntityDescriptorControllerTests extends Specification { def setup() { generator = new TestObjectGenerator() + randomGenerator = new RandomGenerator() mapper = new ObjectMapper() service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) - controller = new EntityDescriptorController ( + controller = new EntityDescriptorController( entityDescriptorRepository: entityDescriptorRepository, openSamlObjects: openSamlObjects, entityDescriptorService: service ) - + controller.restTemplate = mockRestTemplate mockMvc = MockMvcBuilders.standaloneSetup(controller).build() } - def 'GET /EntityDescriptors with empty repository'() { given: def emptyRecordsFromRepository = [].stream() @@ -359,6 +362,250 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(expectedJsonBody, true)) } + def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { + given: + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate)) + entityDescriptor.setElementLocalName("EntityDescriptor") + entityDescriptor.setNamespacePrefix("md") + entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + + def expectedXML = """ +""" + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") + .accept(APPLICATION_XML)) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor + + + result.andExpect(status().isOk()) + .andExpect(content().xml(expectedXML)) + } + + def "POST /EntityDescriptor handles XML happily"() { + given: + def postedBody = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + def spName = randomGenerator.randomString() + + def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(postedBody.bytes)) + + 1 * entityDescriptorRepository.findByEntityID(_) >> null + 1 * entityDescriptorRepository.save(_) >> expectedEntityDescriptor + + def expectedJson = """ +{ + "version": ${expectedEntityDescriptor.hashCode()}, + "id": "${expectedEntityDescriptor.resourceId}", + "serviceProviderName": null, + "entityId": "http://test.scaldingspoon.org/test1", + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": "SAML 2", + "nameIdFormats": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + ] + }, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": [ + { + "locationUrl": "https://test.scaldingspoon.org/test1/acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "makeDefault": false + } + ], + "serviceEnabled": false, + "createdDate": null, + "modifiedDate": null, + "relyingPartyOverrides": { + "signAssertion": false, + "dontSignResponse": false, + "turnOffEncryption": false, + "useSha": false, + "ignoreAuthenticationMethod": false, + "omitNotBefore": false, + "responderId": null, + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [ + "givenName", + "employeeNumber" + ] +} +""" + + when: + def result = mockMvc.perform(post("/api/EntityDescriptor") + .contentType(APPLICATION_XML) + .content(postedBody) + .param("spName", spName)) + + + then: + result.andExpect(status().isCreated()) + .andExpect(content().json(expectedJson, true)) + } + + def "POST /EntityDescriptor returns error for duplicate entity id"() { + given: + def postedBody = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + def spName = randomGenerator.randomString() + + def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(postedBody.bytes)) + + 1 * entityDescriptorRepository.findByEntityID(expectedEntityDescriptor.entityID) >> expectedEntityDescriptor + 0 * entityDescriptorRepository.save(_) + + when: + def result = mockMvc.perform(post("/api/EntityDescriptor") + .contentType(APPLICATION_XML) + .content(postedBody) + .param("spName", spName)) + + + then: + result.andExpect(status().isConflict()) + .andExpect(content().string("The entity descriptor with entity id [http://test.scaldingspoon.org/test1] already exists.")) + } + + def "POST /EntityDescriptor handles x-www-form-urlencoded happily"() { + given: + def postedMetadataUrl = "http://test.scaldingspoon.org/test1" + def restXml = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + + def spName = randomGenerator.randomString() + + def expectedEntityDescriptor = EntityDescriptor.class.cast(openSamlObjects.unmarshalFromXml(restXml.bytes)) + + 1 * mockRestTemplate.getForObject(_, _) >> restXml.bytes + 1 * entityDescriptorRepository.findByEntityID(_) >> null + 1 * entityDescriptorRepository.save(_) >> expectedEntityDescriptor + + def expectedJson = """ +{ + "version": ${expectedEntityDescriptor.hashCode()}, + "id": "${expectedEntityDescriptor.resourceId}", + "serviceProviderName": null, + "entityId": "http://test.scaldingspoon.org/test1", + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": { + "protocolSupportEnum": "SAML 2", + "nameIdFormats": [ + "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" + ] + }, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": [ + { + "locationUrl": "https://test.scaldingspoon.org/test1/acs", + "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + "makeDefault": false + } + ], + "serviceEnabled": false, + "createdDate": null, + "modifiedDate": null, + "relyingPartyOverrides": { + "signAssertion": false, + "dontSignResponse": false, + "turnOffEncryption": false, + "useSha": false, + "ignoreAuthenticationMethod": false, + "omitNotBefore": false, + "responderId": null, + "nameIdFormats": [], + "authenticationMethods": [] + }, + "attributeRelease": [ + "givenName", + "employeeNumber" + ] +} +""" + + when: + def result = mockMvc.perform(post("/api/EntityDescriptor") + .contentType(APPLICATION_FORM_URLENCODED) + .param("metadataUrl", postedMetadataUrl) + .param("spName", spName)) + + + then: + result.andExpect(status().isCreated()) + .andExpect(content().json(expectedJson, true)) + } + def "PUT /EntityDescriptor updates entity descriptors properly"() { given: def entityDescriptor = generator.buildEntityDescriptor() diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy new file mode 100644 index 000000000..a3b8fdf7c --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/scheduled/EntityDescriptorFilesScheduledTasksTests.groovy @@ -0,0 +1,143 @@ +package edu.internet2.tier.shibboleth.admin.ui.scheduled + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class EntityDescriptorFilesScheduledTasksTests extends Specification { + + def tempPath = "/tmp/shibui" + + def directory + + @Autowired + OpenSamlObjects openSamlObjects + + def entityDescriptorRepository = Mock(EntityDescriptorRepository) + + def entityDescriptorFilesScheduledTasks + + def service + + def randomGenerator + + def setup() { + randomGenerator = new RandomGenerator() + tempPath = tempPath + randomGenerator.randomRangeInt(10000, 20000) + service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) + entityDescriptorFilesScheduledTasks = new EntityDescriptorFilesScheduledTasks(tempPath, entityDescriptorRepository, openSamlObjects) + directory = new File(tempPath) + directory.mkdir() + } + + def "generateEntityDescriptorFiles properly generates a file from an Entity Descriptor"() { + given: + def expectedXml = ''' + + + name + display name + http://test.example.org + + + ''' + + def entityDescriptor = service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.organization = new OrganizationRepresentation().with { + it.name = 'name' + it.displayName = 'display name' + it.url = 'http://test.example.org' + it + } + it + }) + 1 * entityDescriptorRepository.findAllByServiceEnabled(true) >> [entityDescriptor].stream() + + when: + if (directory.exists()) { + entityDescriptorFilesScheduledTasks.generateEntityDescriptorFiles() + } else { + throw new RuntimeException("temp directory does not exist!") + } + + then: + def files = new File(tempPath).listFiles({d, f -> f ==~ /.*.xml/ } as FilenameFilter) + files.size() == 1 + def result = files[0].text + def diff = DiffBuilder.compare(Input.fromString(expectedXml)).withTest(Input.fromString(result)).ignoreComments().ignoreWhitespace().build() + !diff.hasDifferences() + } + + def "removeDanglingEntityDescriptorFiles properly deletes files"() { + given: + def expectedXml = ''' + + + name + display name + http://test.example.org + + + ''' + + def entityDescriptor = service.createDescriptorFromRepresentation(new EntityDescriptorRepresentation().with { + it.entityId = 'http://test.example.org/test1' + it.organization = new OrganizationRepresentation().with { + it.name = 'name' + it.displayName = 'display name' + it.url = 'http://test.example.org' + it + } + it + }) + + def file = new File(directory, randomGenerator.randomId() + ".xml") + file.text = "Delete me!" + + 1 * entityDescriptorRepository.findAllByServiceEnabled(true) >> [entityDescriptor].stream() + + when: + entityDescriptorFilesScheduledTasks.removeDanglingEntityDescriptorFiles() + + then: + def files = new File(tempPath, file) + files.size() == 0 + } + + def cleanup() { + directory.deleteDir() + } +} \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy index 0a30fa8bf..396b3df5b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImplTests.groovy @@ -1,8 +1,19 @@ package edu.internet2.tier.shibboleth.admin.ui.service import com.fasterxml.jackson.databind.ObjectMapper -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.* +import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor +import edu.internet2.tier.shibboleth.admin.ui.domain.XSAny +import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import org.springframework.boot.test.json.JacksonTester import org.xmlunit.builder.DiffBuilder @@ -24,8 +35,11 @@ class JPAEntityDescriptorServiceImplTests extends Specification { JacksonTester jacksonTester + RandomGenerator generator + def setup() { JacksonTester.initFields(this, new ObjectMapper()) + generator = new RandomGenerator() testObjectGenerator = new TestObjectGenerator() } @@ -595,6 +609,107 @@ class JPAEntityDescriptorServiceImplTests extends Specification { assert descriptor.getSPSSODescriptor('').getKeyDescriptors()[0].getUse() == null } + def "createAttributeWithBooleanValue properly adds booleans to attributes"() { + given: + def expectedName = "someName" + def expectedFriendlyName = "someFriendlyName" + def randomBoolean = generator.randomBoolean() + + when: + def attribute = service.createAttributeWithBooleanValue(expectedName, expectedFriendlyName, randomBoolean) + + then: + expectedName == attribute.getName() + expectedFriendlyName == attribute.getFriendlyName() + attribute.getAttributeValues().size() == 1 + attribute.getAttributeValues().get(0) instanceof XSBoolean + Boolean.parseBoolean(((XSBoolean)attribute.getAttributeValues().get(0)).getStoredValue()) == randomBoolean + + where: + i << (1..5) + } + + def "createAttributeWithArbitraryValues properly adds additional attributes"() { + given: + def expectedName = "someName" + def expectedFriendlyName = "someFriendlyName" + def attributesArray = [] + for (int index = 0; index < testRunIndex; index++) { + attributesArray.add("additionalAttributes" + index) + } + + + when: + def attribute = service.createAttributeWithArbitraryValues(expectedName, + expectedFriendlyName, + attributesArray) + + then: + expectedName == attribute.getName() + expectedFriendlyName == attribute.getFriendlyName() + attribute.getAttributeValues().size() == testRunIndex + for (int index = 0; index < testRunIndex; index++) { + attribute.getAttributeValues().get(index) instanceof XSAny + ((XSAny)attribute.getAttributeValues().get(index)).getTextContent() == "additionalAttributes" + index + } + + where: + testRunIndex << (1..5) + } + + def "createAttributeWithArbitraryValues adds no attributes when passed no attributes"() { + given: + def expectedName = "someName" + def expectedFriendlyName = "someFriendlyName" + + when: + def attribute = service.createAttributeWithArbitraryValues(expectedName, expectedFriendlyName) + + then: + expectedName == attribute.getName() + expectedFriendlyName == attribute.getFriendlyName() + attribute.getAttributeValues().size() == 0 + } + + def "createAttributeWithArbitraryValues doesn't explode when passed a list of strings"() { + given: + def expectedName = "someName" + def expectedFriendlyName = "someFriendlyName" + List attributesList = new ArrayList() + for (int index = 0; index < testRunIndex; index++) { + attributesList.add("additionalAttributes" + index) + } + + when: + def attribute = service.createAttributeWithArbitraryValues(expectedName, + expectedFriendlyName, + attributesList) + + then: + expectedName == attribute.getName() + expectedFriendlyName == attribute.getFriendlyName() + attribute.getAttributeValues().size() == testRunIndex + for (int index = 0; index < testRunIndex; index++) { + attribute.getAttributeValues().get(index) instanceof XSAny + ((XSAny)attribute.getAttributeValues().get(index)).getTextContent() == "additionalAttributes" + index + } + + where: + testRunIndex << (1..5) + } + + def "updateDescriptorFromRepresentation throws expected exception"() { + given: + def randomEntityDescriptor = generateRandomEntityDescriptor() + def entityDescriptorRepresentation = service.createRepresentationFromDescriptor(randomEntityDescriptor) + + when: + service.updateDescriptorFromRepresentation(randomEntityDescriptor, entityDescriptorRepresentation) + + then: + thrown UnsupportedOperationException + } + def "createRepresentationFromDescriptor creates a representation containing a version that is a hash of the original object"() { given: def entityDescriptor = testObjectGenerator.buildEntityDescriptor() @@ -607,4 +722,15 @@ class JPAEntityDescriptorServiceImplTests extends Specification { def actualVersion = representation.version expectedVersion == actualVersion } + + EntityDescriptor generateRandomEntityDescriptor() { + EntityDescriptor ed = new EntityDescriptor() + + ed.setEntityID(generator.randomId()) + ed.setServiceProviderName(generator.randomString(10)) + ed.setServiceEnabled(generator.randomBoolean()) + ed.setResourceId(generator.randomId()) + + return ed + } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy new file mode 100644 index 000000000..8e212bb90 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityServiceImplTests.groovy @@ -0,0 +1,92 @@ +package edu.internet2.tier.shibboleth.admin.ui.service + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.domain.XSString +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers +import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator +import edu.internet2.tier.shibboleth.admin.util.AttributeUtility +import edu.internet2.tier.shibboleth.admin.util.MDDCConstants +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.test.context.ContextConfiguration +import spock.lang.Specification + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, MetadataResolverConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class JPAEntityServiceImplTests extends Specification { + + @Autowired + OpenSamlObjects openSamlObjects + + @Autowired + AttributeUtility attributeUtility; + + def randomGenerator + def testObjectGenerator + + def service + + def setup() { + service = new JPAEntityServiceImpl(openSamlObjects) + service.attributeUtility = attributeUtility + + randomGenerator = new RandomGenerator() + testObjectGenerator = new TestObjectGenerator(attributeUtility) + } + + def "getAttributeListFromEntityRepresentation builds an appropriate attribute list"() { + given: + def representation = new EntityDescriptorRepresentation() + representation.setAttributeRelease(randomGenerator.randomStringList()) + representation.setRelyingPartyOverrides(testObjectGenerator.buildRelyingPartyOverridesRepresentation()) + + when: + def result = service.getAttributeListFromEntityRepresentation(representation) + + then: + //TODO: Similar to JPAFilterServiceImplTests, should we do a more thorough test or is checking the count sufficient? + result.size == 1 + TestHelpers.determineCountOfAttributesFromRelyingPartyOverrides(representation.getRelyingPartyOverrides()) + } + + def "getAttributeFromAttributeReleaseList builds an attribute properly"() { + given: + def listOfStrings = randomGenerator.randomStringList() + + def expectedAttributeName = MDDCConstants.RELEASE_ATTRIBUTES + def expectedNamespaceURI = "urn:oasis:names:tc:SAML:2.0:assertion" + def expectedElementLocalName = "AttributeValue" + def expectedNamespacePrefix = "saml2" + def expectedSchemaTypeNamespaceURI= "http://www.w3.org/2001/XMLSchema" + def expectedSchemaTypeElementLocalName = "string" + def expectedSchemaTypeNamespacePrefix = "xsd" + + when: + def result = service.getAttributeFromAttributeReleaseList(listOfStrings) + + then: + result.name == expectedAttributeName + result.attributeValues.size == listOfStrings.size + result.attributeValues.each { + listOfStrings.contains(it.value) + it.namespaceURI == expectedNamespaceURI + it.elementLocalName == expectedElementLocalName + it.namespacePrefix == expectedNamespacePrefix + it.schemaTypeNamespaceURI == expectedSchemaTypeNamespaceURI + it.schemaTypeElementLocalName == expectedSchemaTypeElementLocalName + it.schemaTypeNamespacePrefix == expectedSchemaTypeNamespacePrefix + } + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy index 567b6e00a..069555ce7 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAFilterServiceImplTests.groovy @@ -3,12 +3,10 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.MetadataResolverConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.FilterRepresentation -import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator +import edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import edu.internet2.tier.shibboleth.admin.util.AttributeUtility -import org.apache.commons.lang.StringUtils import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.domain.EntityScan import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest @@ -55,7 +53,9 @@ class JPAFilterServiceImplTests extends Specification { //complicated test. Testing size is fairly useful, but it forces us to assume that the attributes are what they //should be. Maybe testing that the attributes are what they should be should be done in a unit test for the //actual method that builds the attributes list? - result.getAttributes().size() == determineCountOfAttributesFromRelyingPartyOverrides(representation) + result.getAttributes().size() == + representation.getAttributeRelease().size() + + TestHelpers.determineCountOfAttributesFromRelyingPartyOverrides(representation.getRelyingPartyOverrides()) result.entityAttributesFilterTarget.value == representation.filterTarget.value result.entityAttributesFilterTarget.entityAttributesFilterTargetType.toString() == representation.filterTarget.type @@ -75,27 +75,11 @@ class JPAFilterServiceImplTests extends Specification { result.version == filter.hashCode() //TODO? See note above, same question. - determineCountOfAttributesFromRelyingPartyOverrides(result) == filter.getAttributes().size() + result.getAttributeRelease().size() + + TestHelpers.determineCountOfAttributesFromRelyingPartyOverrides(result.getRelyingPartyOverrides()) == + filter.getAttributes().size() result.filterTarget.type == filter.entityAttributesFilterTarget.entityAttributesFilterTargetType.toString() result.filterTarget.value == filter.entityAttributesFilterTarget.value } - - int determineCountOfAttributesFromRelyingPartyOverrides(FilterRepresentation representation) { - int count = 0 - - count += representation.getAttributeRelease().size() - RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation = representation.getRelyingPartyOverrides() - count += relyingPartyOverridesRepresentation.authenticationMethods.size() != 0 ? 1 : 0 - count += relyingPartyOverridesRepresentation.dontSignResponse ? 1 : 0 - count += relyingPartyOverridesRepresentation.ignoreAuthenticationMethod ? 1 : 0 - count += relyingPartyOverridesRepresentation.nameIdFormats.size() != 0 ? 1 : 0 - count += relyingPartyOverridesRepresentation.omitNotBefore ? 1 : 0 - count += relyingPartyOverridesRepresentation.signAssertion ? 1 : 0 - count += relyingPartyOverridesRepresentation.turnOffEncryption ? 1 : 0 - count += relyingPartyOverridesRepresentation.useSha ? 1 : 0 - count += StringUtils.isNotBlank(relyingPartyOverridesRepresentation.responderId) ? 1 : 0 - - return count - } } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy new file mode 100644 index 000000000..1a6a085e2 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestHelpers.groovy @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.util + +import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.RelyingPartyOverridesRepresentation +import org.apache.commons.lang.StringUtils + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +class TestHelpers { + static int determineCountOfAttributesFromRelyingPartyOverrides(RelyingPartyOverridesRepresentation relyingPartyOverridesRepresentation) { + int count = 0 + + count += relyingPartyOverridesRepresentation.authenticationMethods.size() != 0 ? 1 : 0 + count += relyingPartyOverridesRepresentation.dontSignResponse ? 1 : 0 + count += relyingPartyOverridesRepresentation.ignoreAuthenticationMethod ? 1 : 0 + count += relyingPartyOverridesRepresentation.nameIdFormats.size() != 0 ? 1 : 0 + count += relyingPartyOverridesRepresentation.omitNotBefore ? 1 : 0 + count += relyingPartyOverridesRepresentation.signAssertion ? 1 : 0 + count += relyingPartyOverridesRepresentation.turnOffEncryption ? 1 : 0 + count += relyingPartyOverridesRepresentation.useSha ? 1 : 0 + count += StringUtils.isNotBlank(relyingPartyOverridesRepresentation.responderId) ? 1 : 0 + + return count + } +}