diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy index 34029e28a..b60c9b0c8 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImpl.groovy @@ -46,12 +46,12 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { // TODO: enhance @Override void reloadFilters(String metadataResolverName) { - ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver)metadataResolver + ChainingMetadataResolver chainingMetadataResolver = (ChainingMetadataResolver) metadataResolver MetadataResolver targetMetadataResolver = chainingMetadataResolver.getResolvers().find { it.id == metadataResolverName } edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver jpaMetadataResolver = metadataResolverRepository.findByName(metadataResolverName) if (targetMetadataResolver && targetMetadataResolver.getMetadataFilter() instanceof MetadataFilterChain) { - MetadataFilterChain metadataFilterChain = (MetadataFilterChain)targetMetadataResolver.getMetadataFilter() + MetadataFilterChain metadataFilterChain = (MetadataFilterChain) targetMetadataResolver.getMetadataFilter() List metadataFilters = new ArrayList<>() @@ -64,7 +64,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { if (entityAttributesFilter.getEntityAttributesFilterTarget().getEntityAttributesFilterTargetType() == EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY) { rules.put( new EntityIdPredicate(entityAttributesFilter.getEntityAttributesFilterTarget().getValue()), - (List)(List)entityAttributesFilter.getAttributes() + (List) (List) entityAttributesFilter.getAttributes() ) } target.setRules(rules) @@ -76,7 +76,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { if (metadataResolver instanceof RefreshableMetadataResolver) { try { - ((RefreshableMetadataResolver)metadataResolver).refresh() + ((RefreshableMetadataResolver) metadataResolver).refresh() } catch (ResolverException e) { log.warn("error refreshing metadataResolver " + metadataResolverName, e) } @@ -99,15 +99,18 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'xsi:schemaLocation': 'urn:mace:shibboleth:2.0:metadata http://shibboleth.net/schema/idp/shibboleth-metadata.xsd urn:mace:shibboleth:2.0:resource http://shibboleth.net/schema/idp/shibboleth-resource.xsd urn:mace:shibboleth:2.0:security http://shibboleth.net/schema/idp/shibboleth-security.xsd urn:oasis:names:tc:SAML:2.0:metadata http://docs.oasis-open.org/security/saml/v2.0/saml-schema-metadata-2.0.xsd urn:oasis:names:tc:SAML:2.0:assertion http://docs.oasis-open.org/security/saml/v2.0/saml-schema-assertion-2.0.xsd' ) { metadataResolverRepository.findAll().each { edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver mr -> - constructXmlNodeForResolver(mr, delegate) { - MetadataFilter( - 'xsi:type': 'SignatureValidation', - 'requireSignedRoot': 'true', - 'certificateFile': '%{idp.home}/credentials/inc-md-cert.pem' - ) - //TODO: enhance - mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> - constructXmlNodeForFilter(filter, delegate) + //TODO: We cannot/do not currently have the code to marshall the internal incommon chaining resolver + if ((mr.type != 'BaseMetadataResolver') && (mr.enabled)) { + constructXmlNodeForResolver(mr, delegate) { + MetadataFilter( + 'xsi:type': 'SignatureValidation', + 'requireSignedRoot': 'true', + 'certificateFile': '%{idp.home}/credentials/inc-md-cert.pem' + ) + //TODO: enhance + mr.metadataFilters.each { edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter filter -> + constructXmlNodeForFilter(filter, delegate) + } } } } @@ -163,7 +166,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } void constructXmlNodeForResolver(FilesystemMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.name, + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'FilesystemMetadataProvider', metadataFile: resolver.metadataFile, @@ -187,7 +190,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } void constructXmlNodeForResolver(DynamicHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.name, + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'DynamicHttpMetadataProvider', requireValidMetadata: !resolver.requireValidMetadata ?: null, failFastInitialization: !resolver.failFastInitialization ?: null, @@ -234,7 +237,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { } void constructXmlNodeForResolver(FileBackedHttpMetadataResolver resolver, def markupBuilderDelegate, Closure childNodes) { - markupBuilderDelegate.MetadataProvider(id: resolver.name, + markupBuilderDelegate.MetadataProvider(id: resolver.xmlId, 'xsi:type': 'FileBackedHTTPMetadataProvider', backingFile: resolver.backingFile, metadataURL: resolver.metadataURL, @@ -279,7 +282,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { sourceManagerRef: resolver.sourceManagerRef, sourceKeyGeneratorRef: resolver.sourceKeyGeneratorRef, - id: resolver.name, + id: resolver.xmlId, 'xsi:type': 'DynamicHttpMetadataProvider', requireValidMetadata: !resolver.requireValidMetadata ?: null, failFastInitialization: !resolver.failFastInitialization ?: null, @@ -314,7 +317,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { def resourceType = resolver.validateAndDetermineResourceType() markupBuilderDelegate.MetadataProvider( - id: resolver.name, + id: resolver.xmlId, 'xsi:type': 'ResourceBackedMetadataProvider', parserPoolRef: resolver.reloadableMetadataResolverAttributes?.parserPoolRef, minRefreshDelay: resolver.reloadableMetadataResolverAttributes?.minRefreshDelay, @@ -324,7 +327,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { resolveViaPredicatesOnly: resolver.reloadableMetadataResolverAttributes?.resolveViaPredicatesOnly ?: null, expirationWarningThreshold: resolver.reloadableMetadataResolverAttributes?.expirationWarningThreshold) { - if(resourceType == SVN) { + if (resourceType == SVN) { MetadataResource( 'xmlns:resource': 'urn:mace:shibboleth:2.0:resource', 'xsi:type': 'resource:SVNResource', @@ -338,8 +341,7 @@ class JPAMetadataResolverServiceImpl implements MetadataResolverService { 'proxyUserName': resolver.svnMetadataResource.proxyUserName, 'proxyPassword': resolver.svnMetadataResource.proxyPassword) - } - else if (resourceType == CLASSPATH) { + } else if (resourceType == CLASSPATH) { MetadataResource( 'xmlns:resource': 'urn:mace:shibboleth:2.0:resource', 'xsi:type': 'resource:ClasspathResource', diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index edcfc4ccb..ca7be77ea 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -21,8 +21,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ResourceBundleMessageSource; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.util.UrlPathHelper; import javax.servlet.http.HttpServletRequest; @@ -78,6 +82,11 @@ public AttributeUtility attributeUtility() { @Autowired Directory directory; + @Autowired + LocaleResolver localeResolver; + + @Autowired + ResourceBundleMessageSource messageSource; @Bean public EntityDescriptorFilesScheduledTasks entityDescriptorFilesScheduledTasks(EntityDescriptorRepository entityDescriptorRepository) { @@ -103,6 +112,13 @@ public EntityIdsSearchService entityIdsSearchService() { }; } + @Bean + public LocaleChangeInterceptor localeChangeInterceptor() { + LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor(); + localeChangeInterceptor.setParamName("lang"); + return localeChangeInterceptor; + } + /** * A WebMvcConfigurer that won't mangle the path for the entities endpoint. * @@ -139,6 +155,11 @@ public String getOriginatingServletPath(HttpServletRequest request) { helper.setUrlDecode(false); configurer.setUrlPathHelper(helper); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(localeChangeInterceptor()); + } }; } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/InternationalizationConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/InternationalizationConfiguration.java new file mode 100644 index 000000000..dc8599865 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/InternationalizationConfiguration.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import edu.internet2.tier.shibboleth.admin.ui.i18n.MappedResourceBundleMessageSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.LocaleResolver; +import org.springframework.web.servlet.i18n.SessionLocaleResolver; + +@Configuration +public class InternationalizationConfiguration { + @Bean + public LocaleResolver localeResolver() { + // TODO if we want to control the order, we can implement our own locale resolver instead of using the SessionLocaleResolver. + SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver(); + return sessionLocaleResolver; + } + + @Bean + public MappedResourceBundleMessageSource messageSource() { + MappedResourceBundleMessageSource source = new MappedResourceBundleMessageSource(); + source.setBasenames("i18n/messages"); + source.setUseCodeAsDefaultMessage(true); + return source; + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesController.java new file mode 100644 index 000000000..566dbe4a3 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesController.java @@ -0,0 +1,25 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller; + +import edu.internet2.tier.shibboleth.admin.ui.i18n.MappedResourceBundleMessageSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.Locale; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@Controller +@RequestMapping(value = "/api/messages") +public class InternationalizationMessagesController { + @Autowired + MappedResourceBundleMessageSource messageSource; + + @GetMapping + public ResponseEntity getAll(Locale locale) { + return ResponseEntity.ok(messageSource.getMessagesMap(locale)); + } +} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataProvidersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataProvidersController.java deleted file mode 100644 index 538cfa5cf..000000000 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataProvidersController.java +++ /dev/null @@ -1,40 +0,0 @@ -package edu.internet2.tier.shibboleth.admin.ui.controller; - -import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -import javax.xml.transform.OutputKeys; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import java.io.IOException; -import java.io.StringWriter; - -@Controller -@RequestMapping(value = "/api/metadataProviders") -public class MetadataProvidersController { - private static final Logger logger = LoggerFactory.getLogger(MetadataProvidersController.class); - - @Autowired - MetadataResolverService metadataResolverService; - - @RequestMapping(produces = "application/xml") - public ResponseEntity getXml() throws IOException, TransformerException { - // TODO: externalize - try (StringWriter writer = new StringWriter()) { - Transformer transformer = TransformerFactory.newInstance().newTransformer(); - transformer.setOutputProperty(OutputKeys.INDENT, "yes"); - transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); - - transformer.transform(new DOMSource(metadataResolverService.generateConfiguration()), new StreamResult(writer)); - return ResponseEntity.ok(writer.toString()); - } - } -} diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java index 5b24f5788..9705ddacc 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversController.java @@ -4,6 +4,7 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver; import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidationService; import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository; +import edu.internet2.tier.shibboleth.admin.ui.service.MetadataResolverService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -20,7 +21,14 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; import java.io.IOException; +import java.io.StringWriter; import java.net.URI; import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolverValidator.ValidationResult; @@ -36,6 +44,9 @@ public class MetadataResolversController { @Autowired MetadataResolverValidationService metadataResolverValidationService; + @Autowired + MetadataResolverService metadataResolverService; + @ExceptionHandler({InvalidTypeIdException.class, IOException.class, HttpMessageNotReadableException.class}) public ResponseEntity unableToParseJson(Exception ex) { return ResponseEntity.badRequest().body(new ErrorResponse(HttpStatus.BAD_REQUEST.toString(), ex.getMessage())); @@ -49,6 +60,20 @@ public ResponseEntity getAll() { return ResponseEntity.ok(resolvers); } + @GetMapping(value = "/MetadataResolvers", produces = "application/xml") + @Transactional(readOnly = true) + public ResponseEntity getXml() throws IOException, TransformerException { + // TODO: externalize + try (StringWriter writer = new StringWriter()) { + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); + + transformer.transform(new DOMSource(metadataResolverService.generateConfiguration()), new StreamResult(writer)); + return ResponseEntity.ok(writer.toString()); + } + } + @GetMapping("/MetadataResolvers/{resourceId}") @Transactional(readOnly = true) public ResponseEntity getOne(@PathVariable String resourceId) { @@ -67,18 +92,16 @@ public ResponseEntity create(@RequestBody MetadataResolver newResolver) { return ResponseEntity.status(HttpStatus.CONFLICT).build(); } - //TODO: we are disregarding attached filters if any sent from UI. - //Only deal with filters via filters endpoints? - newResolver.clearAllFilters(); - ResponseEntity validationErrorResponse = validate(newResolver); if(validationErrorResponse != null) { return validationErrorResponse; } + newResolver.convertFiltersFromTransientRepresentationIfNecessary(); MetadataResolver persistedResolver = resolverRepository.save(newResolver); persistedResolver.updateVersion(); + persistedResolver.convertFiltersIntoTransientRepresentationIfNecessary(); return ResponseEntity.created(getResourceUriFor(persistedResolver)).body(persistedResolver); } @@ -102,8 +125,7 @@ public ResponseEntity update(@PathVariable String resourceId, @RequestBody Me updatedResolver.setAudId(existingResolver.getAudId()); - //TODO: we are disregarding attached filters if any sent from UI. - //Only deal with filters via filters endpoints? + //If one needs to update filters, it should be dealt with via filters endpoints updatedResolver.setMetadataFilters(existingResolver.getMetadataFilters()); MetadataResolver persistedResolver = resolverRepository.save(updatedResolver); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java index e5b1221c1..7d0fc03d1 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/resolvers/MetadataResolver.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable; +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter; import edu.internet2.tier.shibboleth.admin.ui.domain.filters.MetadataFilter; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -23,6 +24,8 @@ import java.util.List; import java.util.UUID; +import static java.util.stream.Collectors.toList; + @Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) @EqualsAndHashCode(callSuper = true, exclude = {"version"}) @@ -48,6 +51,11 @@ public class MetadataResolver extends AbstractAuditable { @Column(unique = true) private String resourceId = UUID.randomUUID().toString(); + @Column(unique = true) + private String xmlId; + + private Boolean enabled = true; + private Boolean requireValidMetadata = true; private Boolean failFastInitialization = true; @@ -71,7 +79,18 @@ public void updateVersion() { this.version = hashCode(); } - public void clearAllFilters() { - this.metadataFilters.clear(); + public void convertFiltersIntoTransientRepresentationIfNecessary() { + getAvailableEntityAttributesFilters().forEach(EntityAttributesFilter::intoTransientRepresentation); + } + + public void convertFiltersFromTransientRepresentationIfNecessary() { + getAvailableEntityAttributesFilters().forEach(EntityAttributesFilter::fromTransientRepresentation); + } + + private List getAvailableEntityAttributesFilters() { + return this.metadataFilters.stream() + .filter(EntityAttributesFilter.class::isInstance) + .map(EntityAttributesFilter.class::cast) + .collect(toList()); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/i18n/MappedResourceBundleMessageSource.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/i18n/MappedResourceBundleMessageSource.java new file mode 100644 index 000000000..672cca880 --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/i18n/MappedResourceBundleMessageSource.java @@ -0,0 +1,28 @@ +package edu.internet2.tier.shibboleth.admin.ui.i18n; + +import org.springframework.context.support.ResourceBundleMessageSource; + +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +public class MappedResourceBundleMessageSource extends ResourceBundleMessageSource { + public Map getMessagesMap(Locale locale) { + ResourceBundle resourceBundle = this.doGetBundle("i18n/messages", locale); + Map messagesMap = new HashMap<>(); + Enumeration bundleKeys = resourceBundle.getKeys(); + + while (bundleKeys.hasMoreElements()) { + String key = (String)bundleKeys.nextElement(); + String value = resourceBundle.getString(key); + messagesMap.put(key, value); + } + + return messagesMap; + } +} diff --git a/backend/src/main/resources/i18n/messages.properties b/backend/src/main/resources/i18n/messages.properties new file mode 100644 index 000000000..d9a5d0df4 --- /dev/null +++ b/backend/src/main/resources/i18n/messages.properties @@ -0,0 +1,10 @@ +# Fill this file with key/value pairs, as follows: +# +# some.test.message=This is a test message. +# +# Then, create a copy using the name of the language code: +# +# messages_.properties +# +# Do this for each language we want to support. +# Ideally, all messages should exist for each language. diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties new file mode 100644 index 000000000..5c101832f --- /dev/null +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -0,0 +1 @@ +a.sample.message=This is a sample message. \ No newline at end of file diff --git a/backend/src/main/resources/i18n/messages_fr.properties b/backend/src/main/resources/i18n/messages_fr.properties new file mode 100644 index 000000000..750c5fe51 --- /dev/null +++ b/backend/src/main/resources/i18n/messages_fr.properties @@ -0,0 +1 @@ +a.sample.message=Le francais est tres difficile. \ No newline at end of file diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesControllerTests.groovy new file mode 100644 index 000000000..48a536d19 --- /dev/null +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/InternationalizationMessagesControllerTests.groovy @@ -0,0 +1,113 @@ +package edu.internet2.tier.shibboleth.admin.ui.controller + +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration +import edu.internet2.tier.shibboleth.admin.ui.i18n.MappedResourceBundleMessageSource +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.springframework.test.web.servlet.setup.MockMvcBuilders +import org.springframework.web.servlet.LocaleResolver +import org.springframework.web.servlet.i18n.LocaleChangeInterceptor +import spock.lang.Specification + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content + +/** + * @author Bill Smith (wsmith@unicon.net) + */ +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") +class InternationalizationMessagesControllerTests extends Specification { + @Autowired + MappedResourceBundleMessageSource messageSource + + @Autowired + LocaleChangeInterceptor localeChangeInterceptor + + @Autowired + LocaleResolver localResolver + + def controller + def mockMvc + + def setup() { + controller = new InternationalizationMessagesController( + messageSource: messageSource + ) + + mockMvc = MockMvcBuilders.standaloneSetup(controller) + .setLocaleResolver(localResolver) + .addInterceptors(localeChangeInterceptor) + .build() + } + + def messagesUrl = "/api/messages" + + def expectedEnglishResult = + '{' + + ' "some.test.message": "This is the English test message."' + + '}' + + def expectedFrenchResult = + '{' + + ' "some.test.message": "Je ne sais pas Francais."' + + '}' + + def "GET messages with no header or \"lang\" param defaults to returning english messages"() { + when: + def result = mockMvc.perform( + get(messagesUrl)) + + then: + result.andExpect(content().json(expectedEnglishResult)) + } + + def "GET messages with Accept-Language returns messages in that language"() { + when: + def result = mockMvc.perform( + get(messagesUrl) + .header("Accept-Language", "fr")) + + then: + result.andExpect(content().json(expectedFrenchResult)) + } + + def "GET messages with \"lang\" request param returns messages in that language"() { + when: + def result = mockMvc.perform( + get(messagesUrl) + .param("lang", "fr")) + + then: + result.andExpect(content().json(expectedFrenchResult)) + } + + def "GET messages with both Accept-Language header and \"lang\" request param returns messages in the language specified by the \"lang\" parameter"() { + when: + def result = mockMvc.perform( + get(messagesUrl) + .header("Accept-Language", "en") + .param("lang", "fr")) + + then: + result.andExpect(content().json(expectedFrenchResult)) + } + + def "GET messages with an unsupported Accept-Language returns the default language"() { + when: + def result = mockMvc.perform( + get(messagesUrl) + .header("Accept-Language", "es")) + + then: + result.andExpect(content().json(expectedEnglishResult)) + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy index d160411db..0443a7e54 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataFiltersControllerTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -35,7 +36,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class MetadataFiltersControllerTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy index cb6592990..25071cb98 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataResolversControllerIntegrationTests.groovy @@ -2,6 +2,8 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolverRepository @@ -53,6 +55,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { generator = new TestObjectGenerator(attributeUtility) mapper = new ObjectMapper() mapper.enable(SerializationFeature.INDENT_OUTPUT) + mapper.registerModule(new JavaTimeModule()) } def cleanup() { @@ -148,7 +151,7 @@ class MetadataResolversControllerIntegrationTests extends Specification { } @Unroll - def "POST new DynamicHttpMetadataResolver of type #resolverType -> /api/MetadataResolvers"(String resolverType) { + def "POST new concrete MetadataResolver of type #resolverType -> /api/MetadataResolvers"(String resolverType) { given: 'New MetadataResolver JSON representation' def resolver = generator.buildRandomMetadataResolverOfType(resolverType) @@ -231,6 +234,27 @@ class MetadataResolversControllerIntegrationTests extends Specification { updatedResult.statusCodeValue == 409 } + def "POST new MetadataResolver with one EntityAttributesFilters attached -> /api/MetadataResolvers"() { + given: 'New MetadataResolver with attached entity attributes filter JSON representation' + def resolver = generator.buildRandomMetadataResolverOfType('FileBacked') + resolver.metadataFilters << generator.entityAttributesFilter() + + when: 'POST request is made with new FileBackedMetadataResolver with EntityAttributesFilter JSON representation' + def result = this.restTemplate.postForEntity(BASE_URI, createRequestHttpEntityFor { mapper.writeValueAsString(resolver) }, String) + + then: + result.statusCodeValue == 201 + result.headers.Location[0].contains(BASE_URI) + + when: 'Query REST API for newly created resolver' + def createdResolverResult = this.restTemplate.getForEntity(result.headers.Location[0], String) + def createdResolver = mapper.readValue(createdResolverResult.body, edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.MetadataResolver) + + then: + createdResolver.metadataFilters.size() == 1 + createdResolver.metadataFilters[0] instanceof EntityAttributesFilter + } + private HttpEntity createRequestHttpEntityFor(Closure jsonBodySupplier) { new HttpEntity(jsonBodySupplier(), ['Content-Type': 'application/json'] as HttpHeaders) } diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy index e10ff5b0b..eb29bf550 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/EnityDescriptorRepositoryTest.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.repository +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -21,7 +22,7 @@ import javax.persistence.EntityManager * A highly unnecessary test so that I can check to make sure that persistence is correct for the model */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EnityDescriptorRepositoryTest extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy index 5b5d90357..46f582501 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FileBackedHttpMetadataResolverRepositoryTests.groovy @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -23,7 +24,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttrib import static edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.HttpMetadataResolverAttributes.HttpCachingType.memory @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(methodMode = DirtiesContext.MethodMode.BEFORE_METHOD) diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy index 3ef170b9d..d7b59df55 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/FilterRepositoryTests.groovy @@ -2,6 +2,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import com.fasterxml.jackson.databind.ObjectMapper import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter @@ -15,7 +16,7 @@ import spock.lang.Specification import javax.persistence.EntityManager @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class FilterRepositoryTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy index 039f3b324..63ae7018c 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/LocalDynamicMetadataResolverRepositoryTests.groovy @@ -1,6 +1,7 @@ package edu.internet2.tier.shibboleth.admin.ui.repository import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter @@ -19,7 +20,7 @@ import javax.persistence.EntityManager import static edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget.EntityAttributesFilterTargetType.ENTITY @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class LocalDynamicMetadataResolverRepositoryTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy index 7179dceee..646d3e4aa 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/repository/MetadataResolverRepositoryTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.repository +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -26,7 +27,7 @@ import javax.persistence.EntityManager * Testing persistence of the MetadataResolver models */ @DataJpaTest -@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class MetadataResolverRepositoryTests extends Specification { 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 index 73eb2c58c..b687e5c1c 100644 --- 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.scheduled +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -23,7 +24,7 @@ import spock.lang.Specification * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntityDescriptorFilesScheduledTasksTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy index 55777e6ec..7b0cc9f4b 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/EntityIdsSearchServiceTests.groovy @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -14,7 +15,7 @@ import spock.lang.Specification * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntityIdsSearchServiceTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy index 24a97ccdb..b158c83d0 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/IncommonJPAMetadataResolverServiceImplTests.groovy @@ -1,6 +1,7 @@ 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.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget @@ -27,7 +28,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.* @SpringBootTest @DataJpaTest -@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration]) +@ContextConfiguration(classes = [CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class IncommonJPAMetadataResolverServiceImplTests extends Specification { @@ -130,9 +131,7 @@ class IncommonJPAMetadataResolverServiceImplTests extends Specification { if (!metadataResolverRepository.findAll().iterator().hasNext()) { //Generate and test edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.FileBackedHttpMetadataResolver. Add more as // we implement them - def mr = new TestObjectGenerator(attributeUtility).fileBackedHttpMetadataResolver() - mr.setName("HTTPMetadata") - metadataResolverRepository.save(mr) + metadataResolverRepository.save(new TestObjectGenerator(attributeUtility).fileBackedHttpMetadataResolver()) // Generate and test edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.DynamicHttpMetadataResolver. metadataResolverRepository.save(new TestObjectGenerator(attributeUtility).dynamicHttpMetadataResolver()) 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 index 607ed24cc..83a5c76f4 100644 --- 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -21,7 +22,7 @@ import spock.lang.Specification * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class JPAEntityServiceImplTests extends Specification { 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 11ceb346f..51a6f89bc 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 @@ -1,5 +1,6 @@ package edu.internet2.tier.shibboleth.admin.ui.service +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration @@ -18,7 +19,7 @@ import spock.lang.Specification * @author Bill Smith (wsmith@unicon.net) */ @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") class JPAFilterServiceImplTests extends Specification { diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy index 9c367c1d6..c6c64e7e1 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JPAMetadataResolverServiceImplTests.groovy @@ -1,6 +1,7 @@ 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.InternationalizationConfiguration import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilter import edu.internet2.tier.shibboleth.admin.ui.domain.filters.EntityAttributesFilterTarget @@ -41,7 +42,7 @@ import static edu.internet2.tier.shibboleth.admin.ui.util.TestHelpers.generatedX @SpringBootTest @DataJpaTest -@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration]) +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, InternationalizationConfiguration]) @EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) @EntityScan("edu.internet2.tier.shibboleth.admin.ui") @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) @@ -176,7 +177,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { def 'test generating ResourceBackedMetadataResolver with SVN resource type xml snippet'() { given: def resolver = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver().with { - it.name = 'SVNResourceMetadata' + it.xmlId = 'SVNResourceMetadata' it.svnMetadataResource = new SvnMetadataResource().with { it.resourceFile = 'entity.xml' it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' @@ -198,7 +199,7 @@ class JPAMetadataResolverServiceImplTests extends Specification { def 'test generating ResourceBackedMetadataResolver with classpath resource type xml snippet'() { given: def resolver = new edu.internet2.tier.shibboleth.admin.ui.domain.resolvers.ResourceBackedMetadataResolver().with { - it.name = 'ClasspathResourceMetadata' + it.xmlId = 'ClasspathResourceMetadata' it.classpathMetadataResource = new ClasspathMetadataResource().with { it.file = '/path/to/a/classpath/location/metadata.xml' it @@ -228,6 +229,19 @@ class JPAMetadataResolverServiceImplTests extends Specification { generatedXmlIsTheSameAsExpectedXml('/conf/520.xml', domBuilder.parseText(writer.toString())) } + def 'test generating disabled MetadataResolver xml snippet'() { + given: 'disabled metadata resolver' + def resolver = testObjectGenerator.filesystemMetadataResolver() + resolver.enabled = false + metadataResolverRepository.save(resolver) + + when: + def generatedXmlDocument = this.metadataResolverService.generateConfiguration() + + then: + generatedXmlIsTheSameAsExpectedXml('/conf/670.xml', generatedXmlDocument) + } + static genXmlSnippet(MarkupBuilder xml, Closure xmlNodeGenerator) { xml.MetadataProvider('id': 'ShibbolethMetadata', 'xmlns': 'urn:mace:shibboleth:2.0:metadata', diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy index 4652fe361..4469e1b36 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/util/TestObjectGenerator.groovy @@ -403,6 +403,7 @@ class TestObjectGenerator { FilesystemMetadataResolver filesystemMetadataResolver() { new FilesystemMetadataResolver().with { it.name = 'FilesystemMetadata' + it.xmlId = 'FilesystemMetadata' it.metadataFile = 'some metadata filename' it.reloadableMetadataResolverAttributes = new ReloadableMetadataResolverAttributes().with { @@ -418,6 +419,7 @@ class TestObjectGenerator { FileBackedHttpMetadataResolver fileBackedHttpMetadataResolver() { new FileBackedHttpMetadataResolver().with { it.name = 'HTTPMetadata' + it.xmlId = 'HTTPMetadata' it.backingFile = '%{idp.home}/metadata/incommonmd.xml' it.metadataURL = 'http://md.incommon.org/InCommon/InCommon-metadata.xml' @@ -434,6 +436,7 @@ class TestObjectGenerator { DynamicHttpMetadataResolver dynamicHttpMetadataResolver() { new DynamicHttpMetadataResolver().with { it.name = 'DynamicHTTP' + it.xmlId = 'DynamicHTTP' it } } @@ -441,6 +444,7 @@ class TestObjectGenerator { LocalDynamicMetadataResolver localDynamicMetadataResolver() { new LocalDynamicMetadataResolver().with { it.name = 'LocalDynamic' + it.xmlId = 'LocalDynamic' it } } @@ -448,6 +452,7 @@ class TestObjectGenerator { ResourceBackedMetadataResolver resourceBackedMetadataResolverForSVN() { new ResourceBackedMetadataResolver().with { it.name = 'SVNResourceMetadata' + it.xmlId = 'SVNResourceMetadata' it.svnMetadataResource = new SvnMetadataResource().with { it.resourceFile = 'entity.xml' it.repositoryURL = 'https://svn.example.org/repo/path/to.dir' diff --git a/backend/src/test/resources/conf/670.xml b/backend/src/test/resources/conf/670.xml new file mode 100644 index 000000000..952c86ee6 --- /dev/null +++ b/backend/src/test/resources/conf/670.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/backend/src/test/resources/i18n/messages.properties b/backend/src/test/resources/i18n/messages.properties new file mode 100644 index 000000000..3e763c8d5 --- /dev/null +++ b/backend/src/test/resources/i18n/messages.properties @@ -0,0 +1 @@ +some.test.message=This is the default message. \ No newline at end of file diff --git a/backend/src/test/resources/i18n/messages_en.properties b/backend/src/test/resources/i18n/messages_en.properties new file mode 100644 index 000000000..3a05d1b5c --- /dev/null +++ b/backend/src/test/resources/i18n/messages_en.properties @@ -0,0 +1 @@ +some.test.message=This is the English test message. \ No newline at end of file diff --git a/backend/src/test/resources/i18n/messages_fr.properties b/backend/src/test/resources/i18n/messages_fr.properties new file mode 100644 index 000000000..23ded1634 --- /dev/null +++ b/backend/src/test/resources/i18n/messages_fr.properties @@ -0,0 +1 @@ +some.test.message=Je ne sais pas Francais. \ No newline at end of file diff --git a/ui/src/app/metadata/domain/model/providers/base-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/base-metadata-provider.ts new file mode 100644 index 000000000..02d9ef01d --- /dev/null +++ b/ui/src/app/metadata/domain/model/providers/base-metadata-provider.ts @@ -0,0 +1,5 @@ +import { MetadataProvider } from '../metadata-provider'; + +export interface BaseMetadataProvider extends MetadataProvider { + metadataFilters: any[]; +} diff --git a/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts b/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts index 7d107036b..1d1e39291 100644 --- a/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts +++ b/ui/src/app/metadata/domain/model/providers/file-backed-http-metadata-provider.ts @@ -1,5 +1,7 @@ -import { MetadataProvider } from '../metadata-provider'; +import { BaseMetadataProvider } from './base-metadata-provider'; -export interface FileBackedHttpMetadataProvider extends MetadataProvider { - metadataFilters: any[]; +export interface FileBackedHttpMetadataProvider extends BaseMetadataProvider { + id: string; + metadataURL: string; + reloadableMetadataResolverAttributes: any; } diff --git a/ui/src/app/metadata/domain/model/providers/index.ts b/ui/src/app/metadata/domain/model/providers/index.ts index 0cd02d47d..6bce32434 100644 --- a/ui/src/app/metadata/domain/model/providers/index.ts +++ b/ui/src/app/metadata/domain/model/providers/index.ts @@ -1 +1,2 @@ -export * from './file-backed-http-metadata-provider'; \ No newline at end of file +export * from './file-backed-http-metadata-provider'; +export * from './base-metadata-provider'; diff --git a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html index cf446c1d1..5690a336e 100644 --- a/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html +++ b/ui/src/app/metadata/manager/container/dashboard-providers-list.component.html @@ -8,7 +8,10 @@
-
{{ providers$ | async | json }}
+ + {{ provider.name }} +
diff --git a/ui/src/app/metadata/provider/action/collection.action.ts b/ui/src/app/metadata/provider/action/collection.action.ts index 99e71c758..7eaf7d1df 100644 --- a/ui/src/app/metadata/provider/action/collection.action.ts +++ b/ui/src/app/metadata/provider/action/collection.action.ts @@ -7,9 +7,13 @@ export enum ProviderCollectionActionTypes { UPDATE_PROVIDER_SUCCESS = '[Metadata Provider] Update Success', UPDATE_PROVIDER_FAIL = '[Metadata Provider] Update Fail', - LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider REQUEST', - LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SUCCESS', - LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider ERROR', + LOAD_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider Load REQUEST', + LOAD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider Load SUCCESS', + LOAD_PROVIDER_ERROR = '[Metadata Provider Collection] Provider Load ERROR', + + SELECT_PROVIDER_REQUEST = '[Metadata Provider Collection] Provider SELECT REQUEST', + SELECT_PROVIDER_SUCCESS = '[Metadata Provider Collection] Provider SELECT SUCCESS', + SELECT_PROVIDER_ERROR = '[Metadata Provider Collection] Provider SELECT ERROR', ADD_PROVIDER_REQUEST = '[Metadata Provider Collection] Add Provider', ADD_PROVIDER_SUCCESS = '[Metadata Provider Collection] Add Provider Success', @@ -38,6 +42,24 @@ export class LoadProviderError implements Action { constructor(public payload: any) { } } +export class SelectProviderRequest implements Action { + readonly type = ProviderCollectionActionTypes.SELECT_PROVIDER_REQUEST; + + constructor(public payload: any) { } +} + +export class SelectProviderSuccess implements Action { + readonly type = ProviderCollectionActionTypes.SELECT_PROVIDER_SUCCESS; + + constructor(public payload: Update) { } +} + +export class SelectProviderError implements Action { + readonly type = ProviderCollectionActionTypes.SELECT_PROVIDER_ERROR; + + constructor(public payload: any) { } +} + export class UpdateProviderRequest implements Action { readonly type = ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST; @@ -96,6 +118,9 @@ export type ProviderCollectionActionsUnion = | LoadProviderRequest | LoadProviderSuccess | LoadProviderError + | SelectProviderRequest + | SelectProviderSuccess + | SelectProviderError | AddProviderRequest | AddProviderSuccess | AddProviderFail diff --git a/ui/src/app/metadata/provider/action/entity.action.ts b/ui/src/app/metadata/provider/action/entity.action.ts index 84f7dbd4a..44c2ca90d 100644 --- a/ui/src/app/metadata/provider/action/entity.action.ts +++ b/ui/src/app/metadata/provider/action/entity.action.ts @@ -2,18 +2,11 @@ import { Action } from '@ngrx/store'; import { MetadataProvider } from '../../domain/model'; export enum EntityActionTypes { - SELECT_PROVIDER = '[Provider Entity] Select Provider', UPDATE_PROVIDER = '[Provider Entity] Update Provider', CLEAR_PROVIDER = '[Provider Entity] Clear', RESET_CHANGES = '[Provider Entity] Reset Changes' } -export class SelectProvider implements Action { - readonly type = EntityActionTypes.SELECT_PROVIDER; - - constructor(public payload: MetadataProvider) { } -} - export class UpdateProvider implements Action { readonly type = EntityActionTypes.UPDATE_PROVIDER; @@ -29,7 +22,6 @@ export class ResetChanges implements Action { } export type EntityActionUnion = - | SelectProvider | UpdateProvider | ClearProvider | ResetChanges; diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts index d951464ad..5c13cc74a 100644 --- a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts +++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.spec.ts @@ -5,7 +5,7 @@ import { StoreModule, Store, combineReducers } from '@ngrx/store'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { ProviderWizardSummaryComponent } from './provider-wizard-summary.component'; +import { ProviderWizardSummaryComponent, getStepProperties } from './provider-wizard-summary.component'; import * as fromRoot from '../reducer'; import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; import * as fromWizard from '../../../wizard/reducer'; @@ -76,6 +76,31 @@ describe('Provider Wizard Summary Component', () => { expect(app).toBeTruthy(); })); + describe('getStepProperties function', () => { + it('should return an empty array of schema or schema.properties is not defined', () => { + expect(getStepProperties(null, {})).toEqual([]); + expect(getStepProperties({}, {})).toEqual([]); + }); + + it('should return a formatted list of properties', () => { + expect(getStepProperties(SCHEMA, {}).length).toBe(2); + }); + }); + + describe('gotoPage function', () => { + it('should emit an empty string if page is null', () => { + spyOn(app.onPageSelect, 'emit'); + app.gotoPage(); + expect(app.onPageSelect.emit).toHaveBeenCalledWith(''); + }); + + it('should emit the provided page', () => { + spyOn(app.onPageSelect, 'emit'); + app.gotoPage('foo'); + expect(app.onPageSelect.emit).toHaveBeenCalledWith('foo'); + }); + }); + describe('ngOnChanges', () => { it('should set columns and sections if summary is provided', () => { instance.summary = { diff --git a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts index 822e51337..c74c4935b 100644 --- a/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts +++ b/ui/src/app/metadata/provider/component/provider-wizard-summary.component.ts @@ -13,7 +13,7 @@ interface Section { properties: Property[]; } -function getStepProperties(schema: any, model: any): Property[] { +export function getStepProperties(schema: any, model: any): Property[] { if (!schema || !schema.properties) { return []; } return Object.keys(schema.properties).map(property => ({ name: schema.properties[property].title, diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.html b/ui/src/app/metadata/provider/container/provider-edit-step.component.html new file mode 100644 index 000000000..5d07730fd --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.html @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.spec.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.spec.ts new file mode 100644 index 000000000..012f1f1c0 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.spec.ts @@ -0,0 +1,90 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProviderEditStepComponent } from './provider-edit-step.component'; +import * as fromRoot from '../reducer'; +import * as fromWizard from '../../../wizard/reducer'; +import { SchemaFormModule, WidgetRegistry, DefaultWidgetRegistry } from 'ngx-schema-form'; +import { SharedModule } from '../../../shared/shared.module'; +import { SetDefinition } from '../../../wizard/action/wizard.action'; +import { FileBackedHttpMetadataProviderEditor } from '../model'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ProviderEditStepComponent) + public componentUnderTest: ProviderEditStepComponent; +} + +describe('Provider Edit Step Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ProviderEditStepComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule.forRoot(), + RouterTestingModule, + SchemaFormModule.forRoot(), + SharedModule, + StoreModule.forRoot({ + provider: combineReducers(fromRoot.reducers), + wizard: combineReducers(fromWizard.reducers, { + wizard: { + index: 'common', + disabled: false, + definition: FileBackedHttpMetadataProviderEditor, + schemaCollection: [] + } + }) + }) + ], + declarations: [ + ProviderEditStepComponent, + TestHostComponent + ], + providers: [ + { provide: WidgetRegistry, useClass: DefaultWidgetRegistry } + ] + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should instantiate the component', async(() => { + expect(app).toBeTruthy(); + })); + + describe('updateStatus method', () => { + it('should update the status with provided errors', () => { + app.currentPage = 'common'; + app.updateStatus({value: 'common'}); + app.updateStatus({value: 'foo'}); + expect(store.dispatch).toHaveBeenCalledTimes(2); + }); + }); + + describe('valueChangeEmitted$ subject', () => { + it('should update the provider', fakeAsync(() => { + app.valueChangeSubject.next({value: { name: 'foo' } }); + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(store.dispatch).toHaveBeenCalled(); + })); + }); +}); diff --git a/ui/src/app/metadata/provider/container/provider-edit-step.component.ts b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts new file mode 100644 index 000000000..63256e8a6 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit-step.component.ts @@ -0,0 +1,93 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import * as fromProvider from '../reducer'; +import { UpdateStatus } from '../action/editor.action'; +import { Wizard } from '../../../wizard/model'; +import { MetadataProvider } from '../../domain/model'; + +import * as fromWizard from '../../../wizard/reducer'; +import { withLatestFrom, map, skipWhile, distinctUntilChanged } from 'rxjs/operators'; +import { UpdateProvider } from '../action/entity.action'; + +@Component({ + selector: 'provider-edit-step', + templateUrl: './provider-edit-step.component.html', + styleUrls: [] +}) + +export class ProviderEditStepComponent implements OnDestroy { + valueChangeSubject = new Subject>(); + private valueChangeEmitted$ = this.valueChangeSubject.asObservable(); + + statusChangeSubject = new Subject>(); + private statusChangeEmitted$ = this.statusChangeSubject.asObservable(); + + currentPage: string; + + namesList: string[] = []; + + schema$: Observable; + provider$: Observable; + model$: Observable; + definition$: Observable>; + changes$: Observable; + + validators$: Observable<{ [key: string]: any }>; + + constructor( + private store: Store + ) { + this.schema$ = this.store.select(fromProvider.getSchema); + this.definition$ = this.store.select(fromWizard.getWizardDefinition); + this.changes$ = this.store.select(fromProvider.getEntityChanges); + this.provider$ = this.store.select(fromProvider.getSelectedProvider); + + this.validators$ = this.store.select(fromProvider.getProviderNames).pipe( + withLatestFrom(this.definition$, this.provider$), + map(([names, def, provider]) => def.getValidators(names.filter(n => n !== provider.name))) + ); + + this.model$ = this.schema$.pipe( + withLatestFrom( + this.store.select(fromProvider.getSelectedProvider), + this.store.select(fromWizard.getModel), + this.changes$, + this.definition$ + ), + map(([schema, provider, model, changes, definition]) => ({ + model: { + ...model, + ...provider, + ...changes + }, + definition + })), + skipWhile(({ model, definition }) => !definition || !model), + map(({ model, definition }) => definition.translate.formatter(model)) + ); + + this.valueChangeEmitted$.pipe( + map(changes => changes.value), + withLatestFrom(this.definition$), + skipWhile(([ changes, definition ]) => !definition || !changes), + map(([ changes, definition ]) => definition.translate.parser(changes)) + ) + .subscribe(changes => this.store.dispatch(new UpdateProvider(changes))); + + this.statusChangeEmitted$.pipe(distinctUntilChanged()).subscribe(errors => this.updateStatus(errors)); + + this.store.select(fromWizard.getWizardIndex).subscribe(i => this.currentPage = i); + } + + updateStatus(errors: any): void { + const status = { [this.currentPage]: !(errors.value) ? 'VALID' : 'INVALID' }; + this.store.dispatch(new UpdateStatus(status)); + } + + ngOnDestroy() { + this.valueChangeSubject.complete(); + } +} + diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.html b/ui/src/app/metadata/provider/container/provider-edit.component.html new file mode 100644 index 000000000..98c8b0f64 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit.component.html @@ -0,0 +1,87 @@ +
+
+
+
+ + Edit Metadata Provider - {{ (provider$ | async).name }} +
+
+
+
+
+
+ +
+
+ +   + +
+
+
+ + All forms must be valid before changes can be saved! +
+
+
+
+
+
+ +
+
+ +
+
+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts b/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts new file mode 100644 index 000000000..096a269c7 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit.component.spec.ts @@ -0,0 +1,115 @@ +import { Component, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { APP_BASE_HREF } from '@angular/common'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProviderEditComponent } from './provider-edit.component'; +import * as fromRoot from '../reducer'; +import * as fromWizard from '../../../wizard/reducer'; +import { SharedModule } from '../../../shared/shared.module'; +import { ActivatedRouteStub } from '../../../../testing/activated-route.stub'; +import { FileBackedHttpMetadataProviderEditor } from '../model'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ProviderEditComponent) + public componentUnderTest: ProviderEditComponent; +} + +describe('Provider Edit Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ProviderEditComponent; + let store: Store; + let router: Router; + let activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); + let child: ActivatedRouteStub = new ActivatedRouteStub(); + child.testParamMap = { form: 'common' }; + activatedRoute.firstChild = child; + + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule.forRoot(), + RouterTestingModule, + SharedModule, + StoreModule.forRoot({ + provider: combineReducers(fromRoot.reducers), + wizard: combineReducers(fromWizard.reducers, { + wizard: { + index: 'common', + disabled: false, + definition: FileBackedHttpMetadataProviderEditor, + schemaCollection: [] + } + }) + }) + ], + declarations: [ + ProviderEditComponent, + TestHostComponent + ], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: APP_BASE_HREF, useValue: '/' } + ] + }).compileComponents(); + + store = TestBed.get(Store); + router = TestBed.get(Router); + spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should instantiate the component', async(() => { + expect(app).toBeTruthy(); + })); + + describe('setIndex method', () => { + it('should interrupt event default and dispatch an event', () => { + const ev = { + preventDefault: jasmine.createSpy('preventDefault'), + stopPropagation: jasmine.createSpy('stopPropagation') + }; + app.setIndex(ev, 'common'); + expect(ev.preventDefault).toHaveBeenCalled(); + expect(ev.stopPropagation).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('go method', () => { + it('should route to the given child form', () => { + spyOn(router, 'navigate'); + app.go('common'); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + + describe('save method', () => { + it('should route to the given child form', () => { + app.save(); + expect(store.dispatch).toHaveBeenCalled(); + }); + }); + + describe('cancel method', () => { + it('should route to the metadata manager', () => { + spyOn(router, 'navigate'); + app.cancel(); + expect(router.navigate).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/src/app/metadata/provider/container/provider-edit.component.ts b/ui/src/app/metadata/provider/container/provider-edit.component.ts new file mode 100644 index 000000000..79f5cb2a6 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-edit.component.ts @@ -0,0 +1,107 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { skipWhile, map, combineLatest } from 'rxjs/operators'; +import { Store } from '@ngrx/store'; +import * as fromWizard from '../../../wizard/reducer'; +import * as fromProvider from '../reducer'; +import { ClearWizard, SetDefinition, SetIndex } from '../../../wizard/action/wizard.action'; +import { ClearEditor, LoadSchemaRequest } from '../action/editor.action'; +import { MetadataProvider } from '../../domain/model'; +import { ClearProvider } from '../action/entity.action'; +import { MetadataProviderEditorTypes } from '../model'; +import { Wizard, WizardStep } from '../../../wizard/model'; +import { UpdateProviderRequest } from '../action/collection.action'; + +@Component({ + selector: 'provider-edit', + templateUrl: './provider-edit.component.html', + styleUrls: [] +}) + +export class ProviderEditComponent implements OnDestroy { + + provider$: Observable; + definition$: Observable>; + index$: Observable; + invalidForms$: Observable; + currentPage$: Observable; + + valid$: Observable; + isInvalid$: Observable; + status$: Observable; + + latest: MetadataProvider; + + constructor( + private store: Store, + private router: Router, + private route: ActivatedRoute + ) { + this.provider$ = this.store.select(fromProvider.getSelectedProvider).pipe(skipWhile(d => !d)); + this.definition$ = this.store.select(fromWizard.getWizardDefinition).pipe(skipWhile(d => !d)); + this.index$ = this.store.select(fromWizard.getWizardIndex).pipe(skipWhile(i => !i)); + this.valid$ = this.store.select(fromProvider.getEditorIsValid); + this.isInvalid$ = this.valid$.pipe(map(v => !v)); + this.status$ = this.store.select(fromProvider.getInvalidEditorForms); + + let startIndex$ = this.route.firstChild ? + this.route.firstChild.params.pipe(map(p => p.form || 'filter-list')) : + this.definition$.pipe(map(d => d.steps[0].id)); + + startIndex$ + .subscribe(index => { + this.store.dispatch(new SetIndex(index)); + }); + + this.provider$ + .subscribe(provider => { + this.store.dispatch(new SetDefinition({ + ...MetadataProviderEditorTypes.find(def => def.type === provider['@type']) + })); + }); + + this.index$.subscribe(id => this.go(id)); + + this.store + .select(fromWizard.getCurrentWizardSchema) + .pipe(skipWhile(s => !s)) + .subscribe(s => { + if (s) { + this.store.dispatch(new LoadSchemaRequest(s)); + } + }); + + this.store.select(fromProvider.getEntityChanges).subscribe(changes => this.latest = changes); + + this.invalidForms$ = this.store.select(fromProvider.getInvalidEditorForms); + this.currentPage$ = this.index$.pipe( + combineLatest(this.definition$, (index, definition) => (definition.steps.find(r => r.id === index))) + ); + } + + go(id: string): void { + this.router.navigate(['./', id], { relativeTo: this.route }); + } + + setIndex($event: Event, id: string): void { + $event.preventDefault(); + $event.stopPropagation(); + this.store.dispatch(new SetIndex(id)); + } + + ngOnDestroy() { + this.store.dispatch(new ClearProvider()); + this.store.dispatch(new ClearWizard()); + this.store.dispatch(new ClearEditor()); + } + + save(): void { + this.store.dispatch(new UpdateProviderRequest(this.latest)); + } + + cancel(): void { + this.router.navigate(['metadata', 'manager', 'providers']); + } +} + diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.html b/ui/src/app/metadata/provider/container/provider-filter-list.component.html new file mode 100644 index 000000000..a1c8e0b1d --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.html @@ -0,0 +1,5 @@ +
+
+ Filter list. +
+
\ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts b/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts new file mode 100644 index 000000000..dd8c2ef95 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.spec.ts @@ -0,0 +1,56 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProviderFilterListComponent } from './provider-filter-list.component'; +import * as fromRoot from '../reducer'; +import * as fromWizard from '../../../wizard/reducer'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ProviderFilterListComponent) + public componentUnderTest: ProviderFilterListComponent; +} + +describe('Provider Filter List Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ProviderFilterListComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule.forRoot(), + RouterTestingModule, + StoreModule.forRoot({ + provider: combineReducers(fromRoot.reducers), + wizard: combineReducers(fromWizard.reducers) + }) + ], + declarations: [ + ProviderFilterListComponent, + TestHostComponent + ], + providers: [] + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should instantiate the component', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/provider/container/provider-filter-list.component.ts b/ui/src/app/metadata/provider/container/provider-filter-list.component.ts new file mode 100644 index 000000000..a35bb9fc3 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-filter-list.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import * as fromProvider from '../reducer'; + +@Component({ + selector: 'provider-filter-list', + templateUrl: './provider-filter-list.component.html', + styleUrls: [] +}) +export class ProviderFilterListComponent { + constructor( + private store: Store + ) { } +} diff --git a/ui/src/app/metadata/provider/container/provider-select.component.html b/ui/src/app/metadata/provider/container/provider-select.component.html new file mode 100644 index 000000000..90c6b6463 --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-select.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-select.component.spec.ts b/ui/src/app/metadata/provider/container/provider-select.component.spec.ts new file mode 100644 index 000000000..b8e07460b --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-select.component.spec.ts @@ -0,0 +1,56 @@ +import { Component, ViewChild } from '@angular/core'; +import { TestBed, async, ComponentFixture } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { StoreModule, Store, combineReducers } from '@ngrx/store'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { ProviderSelectComponent } from './provider-select.component'; +import * as fromRoot from '../reducer'; +import * as fromWizard from '../../../wizard/reducer'; + +@Component({ + template: ` + + ` +}) +class TestHostComponent { + @ViewChild(ProviderSelectComponent) + public componentUnderTest: ProviderSelectComponent; +} + +describe('Provider Select Component', () => { + + let fixture: ComponentFixture; + let instance: TestHostComponent; + let app: ProviderSelectComponent; + let store: Store; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + NgbDropdownModule.forRoot(), + RouterTestingModule, + StoreModule.forRoot({ + provider: combineReducers(fromRoot.reducers), + wizard: combineReducers(fromWizard.reducers) + }) + ], + declarations: [ + ProviderSelectComponent, + TestHostComponent + ], + providers: [] + }).compileComponents(); + + store = TestBed.get(Store); + spyOn(store, 'dispatch'); + + fixture = TestBed.createComponent(TestHostComponent); + instance = fixture.componentInstance; + app = instance.componentUnderTest; + fixture.detectChanges(); + })); + + it('should instantiate the component', async(() => { + expect(app).toBeTruthy(); + })); +}); diff --git a/ui/src/app/metadata/provider/container/provider-select.component.ts b/ui/src/app/metadata/provider/container/provider-select.component.ts new file mode 100644 index 000000000..dc0bcaa6b --- /dev/null +++ b/ui/src/app/metadata/provider/container/provider-select.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import { ActivatedRoute } from '@angular/router'; +import { map, distinctUntilChanged } from 'rxjs/operators'; +import { SelectProviderRequest } from '../action/collection.action'; +import * as fromProviders from '../reducer'; + +@Component({ + selector: 'provider-select', + templateUrl: './provider-select.component.html', + styleUrls: [] +}) + +export class ProviderSelectComponent implements OnDestroy { + actionsSubscription: Subscription; + + constructor( + store: Store, + route: ActivatedRoute + ) { + this.actionsSubscription = route.params.pipe( + distinctUntilChanged(), + map(params => new SelectProviderRequest(params.providerId)) + ).subscribe(store); + } + + ngOnDestroy() { + this.actionsSubscription.unsubscribe(); + } +} + diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.html b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html index 831e71fbb..5d07730fd 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.html +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.html @@ -1,6 +1,8 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts index 7d09b8ec1..dce4187d7 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard-step.component.ts @@ -10,7 +10,7 @@ import { SetDefinition } from '../../../wizard/action/wizard.action'; import { UpdateStatus } from '../action/editor.action'; import { Wizard } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; -import { MetadataProviderTypes, MetadataProviderWizard } from '../model'; +import { MetadataProviderWizardTypes, MetadataProviderWizard } from '../model'; import { UpdateProvider } from '../action/entity.action'; import { pick } from '../../../shared/util'; @@ -37,32 +37,7 @@ export class ProviderWizardStepComponent implements OnDestroy { namesList: string[] = []; - validators = { - '/': (value, property, form_current) => { - let errors; - // iterate all customer - Object.keys(value).forEach((key) => { - const item = value[key]; - const validatorKey = `/${key}`; - const validator = this.validators.hasOwnProperty(validatorKey) ? this.validators[validatorKey] : null; - const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; - if (error) { - errors = errors || []; - errors.push(error); - } - }); - return errors; - }, - '/name': (value, property, form) => { - const err = this.namesList.indexOf(value) > -1 ? { - code: 'INVALID_NAME', - path: `#${property.path}`, - message: 'Name must be unique.', - params: [value] - } : null; - return err; - } - }; + validators$: Observable<{ [key: string]: any }>; constructor( private store: Store, @@ -71,7 +46,10 @@ export class ProviderWizardStepComponent implements OnDestroy { this.definition$ = this.store.select(fromWizard.getWizardDefinition); this.changes$ = this.store.select(fromProvider.getEntityChanges); - this.store.select(fromProvider.getProviderNames).subscribe(list => this.namesList = list); + this.validators$ = this.store.select(fromProvider.getProviderNames).pipe( + withLatestFrom(this.definition$), + map(([names, def]) => def.getValidators(names)) + ); this.model$ = this.schema$.pipe( withLatestFrom( @@ -106,7 +84,7 @@ export class ProviderWizardStepComponent implements OnDestroy { resetSelectedType(changes: any, schema: any, definition: any): { changes: any, definition: any } { const type = changes.value['@type']; if (type && type !== definition.type) { - const newDefinition = MetadataProviderTypes.find(def => def.type === type); + const newDefinition = MetadataProviderWizardTypes.find(def => def.type === type); if (newDefinition) { this.store.dispatch(new SetDefinition({ ...MetadataProviderWizard, diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.html b/ui/src/app/metadata/provider/container/provider-wizard.component.html index 196081a1f..7d1528b03 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.html +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.html @@ -1,17 +1,29 @@ -
- -
-
-
- +
+
+
+
+ Add a new metadata provider +
- - -
+
+
+ +
+
+
+ +
+
+ + +
+
+ + diff --git a/ui/src/app/metadata/provider/container/provider-wizard.component.ts b/ui/src/app/metadata/provider/container/provider-wizard.component.ts index ee18b72b0..014061305 100644 --- a/ui/src/app/metadata/provider/container/provider-wizard.component.ts +++ b/ui/src/app/metadata/provider/container/provider-wizard.component.ts @@ -4,7 +4,7 @@ import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; import * as fromWizard from '../../../wizard/reducer'; -import { SetIndex, SetDisabled, ClearWizard } from '../../../wizard/action/wizard.action'; +import { SetIndex, SetDisabled, ClearWizard, SetDefinition } from '../../../wizard/action/wizard.action'; import { LoadSchemaRequest, ClearEditor } from '../action/editor.action'; import { startWith } from 'rxjs/operators'; import { Wizard, WizardStep } from '../../../wizard/model'; @@ -13,7 +13,7 @@ import { ClearProvider } from '../action/entity.action'; import { Router, ActivatedRoute } from '@angular/router'; import { map } from 'rxjs/operators'; import { AddProviderRequest } from '../action/collection.action'; - +import { MetadataProviderWizard } from '../model'; @Component({ selector: 'provider-wizard', @@ -43,7 +43,9 @@ export class ProviderWizardComponent implements OnDestroy { this.store .select(fromWizard.getCurrentWizardSchema) .subscribe(s => { - this.store.dispatch(new LoadSchemaRequest(s)); + if (s) { + this.store.dispatch(new LoadSchemaRequest(s)); + } }); this.valid$ = this.store.select(fromProvider.getEditorIsValid); this.changes$ = this.store.select(fromProvider.getEntityChanges); @@ -67,6 +69,9 @@ export class ProviderWizardComponent implements OnDestroy { ); this.changes$.subscribe(c => this.provider = c); + + this.store.dispatch(new SetDefinition(MetadataProviderWizard)); + this.store.dispatch(new SetIndex(MetadataProviderWizard.steps[0].id)); } ngOnDestroy() { diff --git a/ui/src/app/metadata/provider/container/provider.component.html b/ui/src/app/metadata/provider/container/provider.component.html index 203223445..8d471248b 100644 --- a/ui/src/app/metadata/provider/container/provider.component.html +++ b/ui/src/app/metadata/provider/container/provider.component.html @@ -1,14 +1,3 @@ -
-
-
-
-
- Add a new metadata provider -
-
-
-
- -
-
+
+
\ No newline at end of file diff --git a/ui/src/app/metadata/provider/container/provider.component.ts b/ui/src/app/metadata/provider/container/provider.component.ts index afaf48a27..c23dc7066 100644 --- a/ui/src/app/metadata/provider/container/provider.component.ts +++ b/ui/src/app/metadata/provider/container/provider.component.ts @@ -1,9 +1,6 @@ import { Component } from '@angular/core'; import { Store } from '@ngrx/store'; import * as fromProvider from '../reducer'; -import { SetDefinition, SetIndex } from '../../../wizard/action/wizard.action'; - -import { MetadataProviderWizard } from '../model'; @Component({ selector: 'provider-page', @@ -13,8 +10,5 @@ import { MetadataProviderWizard } from '../model'; export class ProviderComponent { constructor( private store: Store - ) { - this.store.dispatch(new SetDefinition(MetadataProviderWizard)); - this.store.dispatch(new SetIndex(MetadataProviderWizard.steps[0].id)); - } + ) {} } diff --git a/ui/src/app/metadata/provider/effect/collection.effect.ts b/ui/src/app/metadata/provider/effect/collection.effect.ts index 1a737839e..d4eb5ff0f 100644 --- a/ui/src/app/metadata/provider/effect/collection.effect.ts +++ b/ui/src/app/metadata/provider/effect/collection.effect.ts @@ -1,9 +1,10 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; import { Router } from '@angular/router'; import { of } from 'rxjs'; -import { map, catchError, switchMap, tap } from 'rxjs/operators'; +import { map, catchError, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { ProviderCollectionActionsUnion, ProviderCollectionActionTypes, @@ -12,9 +13,17 @@ import { AddProviderFail, LoadProviderRequest, LoadProviderSuccess, - LoadProviderError + LoadProviderError, + SelectProviderRequest, + SelectProviderSuccess, + SelectProviderError, + UpdateProviderRequest, + UpdateProviderSuccess, + UpdateProviderFail } from '../action/collection.action'; import { MetadataProviderService } from '../../domain/service/provider.service'; +import * as fromProvider from '../reducer'; + /* istanbul ignore next */ @Injectable() @@ -33,6 +42,20 @@ export class CollectionEffects { ) ); + @Effect() + selectProviders$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.SELECT_PROVIDER_REQUEST), + map(action => action.payload), + switchMap(id => + this.providerService + .find(id) + .pipe( + map(provider => new SelectProviderSuccess({ id, changes: provider })), + catchError(error => of(new SelectProviderError(error))) + ) + ) + ); + @Effect() createProvider$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.ADD_PROVIDER_REQUEST), @@ -51,7 +74,37 @@ export class CollectionEffects { createProviderSuccessRedirect$ = this.actions$.pipe( ofType(ProviderCollectionActionTypes.ADD_PROVIDER_SUCCESS), map(action => action.payload), - tap(provider => this.router.navigate(['metadata'])) + tap(provider => this.router.navigate(['metadata', 'manager', 'providers'])) + ); + + @Effect() + updateProvider$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_REQUEST), + map(action => action.payload), + withLatestFrom(this.store.select(fromProvider.getSelectedProvider)), + map(([updates, original]) => ({ ...original, ...updates })), + switchMap(provider => + this.providerService + .update(provider) + .pipe( + map(p => new UpdateProviderSuccess({id: p.id, changes: p})), + catchError((e) => of(new UpdateProviderFail(e))) + ) + ) + ); + + @Effect({ dispatch: false }) + updateProviderSuccessReload$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS), + map(action => action.payload), + tap(provider => this.store.dispatch(new LoadProviderRequest())) + ); + + @Effect({ dispatch: false }) + updateProviderSuccessRedirect$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.UPDATE_PROVIDER_SUCCESS), + map(action => action.payload), + tap(provider => this.router.navigate(['metadata', 'manager', 'providers'])) ); @Effect() @@ -64,6 +117,7 @@ export class CollectionEffects { constructor( private actions$: Actions, private router: Router, + private store: Store, private providerService: MetadataProviderService ) { } } /* istanbul ignore next */ diff --git a/ui/src/app/metadata/provider/effect/editor.effect.ts b/ui/src/app/metadata/provider/effect/editor.effect.ts index bbf1bcc48..3146ff91d 100644 --- a/ui/src/app/metadata/provider/effect/editor.effect.ts +++ b/ui/src/app/metadata/provider/effect/editor.effect.ts @@ -8,7 +8,7 @@ import { LoadSchemaFail, EditorActionTypes } from '../action/editor.action'; -import { map, switchMap, catchError, withLatestFrom } from 'rxjs/operators'; +import { map, switchMap, catchError, withLatestFrom, debounceTime } from 'rxjs/operators'; import { of } from 'rxjs'; import { SetDefinition, WizardActionTypes, AddSchema } from '../../../wizard/action/wizard.action'; import { ResetChanges } from '../action/entity.action'; @@ -23,6 +23,7 @@ export class EditorEffects { $loadSchemaRequest = this.actions$.pipe( ofType(EditorActionTypes.LOAD_SCHEMA_REQUEST), map(action => action.payload), + debounceTime(100), switchMap((schemaPath: string) => this.schemaService .get(schemaPath) diff --git a/ui/src/app/metadata/provider/effect/entity.effect.ts b/ui/src/app/metadata/provider/effect/entity.effect.ts new file mode 100644 index 000000000..36f5faa5a --- /dev/null +++ b/ui/src/app/metadata/provider/effect/entity.effect.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions, ofType } from '@ngrx/effects'; +import { Store } from '@ngrx/store'; +import { map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; + +import { SelectProviderSuccess, ProviderCollectionActionTypes } from '../action/collection.action'; + +import * as fromProvider from '../reducer'; +import { MetadataProvider } from '../../domain/model'; +import { UpdateProvider } from '../action/entity.action'; + +@Injectable() +export class EntityEffects { + + /* + @Effect() + loadModelSuccess$ = this.actions$.pipe( + ofType(ProviderCollectionActionTypes.SELECT_PROVIDER_SUCCESS), + map(action => action.payload.changes), + switchMap((provider: MetadataProvider) => of(new UpdateProvider(provider))) + ); + */ + + constructor( + private store: Store, + private actions$: Actions + ) { } +} diff --git a/ui/src/app/metadata/provider/model/base.provider.form.spec.ts b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts new file mode 100644 index 000000000..307368ef2 --- /dev/null +++ b/ui/src/app/metadata/provider/model/base.provider.form.spec.ts @@ -0,0 +1,142 @@ +import { BaseMetadataProviderEditor } from './base.provider.form'; + +describe('BaseMetadataProviderForm', () => { + + const parser = BaseMetadataProviderEditor.translate.parser; + const formatter = BaseMetadataProviderEditor.translate.formatter; + + const requiredValidUntilFilter = { + maxValidityInterval: 1, + '@type': 'RequiredValidUntil' + }; + + const signatureValidationFilter = { + requireSignedRoot: true, + certificateFile: 'foo', + '@type': 'SignatureValidation' + }; + + const entityRoleWhiteListFilter = { + retainedRoles: ['foo', 'bar'], + removeRolelessEntityDescriptors: true, + removeEmptyEntitiesDescriptors: true, + '@type': 'EntityRoleWhiteList' + }; + + describe('parser', () => { + it('should transform the filters object to an array', () => { + let model = { + name: 'foo', + '@type': 'FileBackedHttpMetadataProvider', + enabled: true, + resourceId: 'foo', + metadataFilters: { + RequiredValidUntil: requiredValidUntilFilter, + SignatureValidation: signatureValidationFilter, + EntityRoleWhiteList: entityRoleWhiteListFilter + } + }; + expect( + parser(model) + ).toEqual( + { + ...model, + metadataFilters: [ + requiredValidUntilFilter, + signatureValidationFilter, + entityRoleWhiteListFilter + ] + } + ); + }); + + it('should return the object if metadataFilters is not provided', () => { + let model = { + name: 'foo', + '@type': 'FileBackedHttpMetadataProvider', + enabled: true, + resourceId: 'foo' + }; + expect( + parser(model) + ).toEqual( + model + ); + }); + }); + + describe('formatter', () => { + it('should transform the filters object to an array', () => { + let model = { + name: 'foo', + '@type': 'FileBackedHttpMetadataProvider', + enabled: true, + resourceId: 'foo', + metadataFilters: [ + requiredValidUntilFilter, + signatureValidationFilter, + entityRoleWhiteListFilter + ] + }; + expect( + formatter(model) + ).toEqual( + { + ...model, + metadataFilters: { + RequiredValidUntil: requiredValidUntilFilter, + SignatureValidation: signatureValidationFilter, + EntityRoleWhiteList: entityRoleWhiteListFilter + } + } + ); + }); + + it('should return the object if metadataFilters is not provided', () => { + let model = { + name: 'foo', + '@type': 'FileBackedHttpMetadataProvider', + enabled: true, + resourceId: 'foo' + }; + expect( + formatter(model) + ).toEqual( + model + ); + }); + }); + + describe('getValidators', () => { + it('should return a set of validator functions for the provider type', () => { + const validators = BaseMetadataProviderEditor.getValidators([]); + expect(validators).toBeDefined(); + expect(validators['/']).toBeDefined(); + expect(validators['/name']).toBeDefined(); + }); + + describe('name `/name` validator', () => { + const validators = BaseMetadataProviderEditor.getValidators(['foo', 'bar']); + + it('should return an invalid object when provided values are invalid based on name', () => { + expect(validators['/name']('foo', { path: '/name' })).toBeDefined(); + }); + + it('should return null when provided values are valid based on name', () => { + expect(validators['/name']('baz', { path: '/name' })).toBeNull(); + }); + }); + + describe('parent `/` validator', () => { + const validators = BaseMetadataProviderEditor.getValidators(['foo', 'bar']); + + it('should return a list of child errors', () => { + expect(validators['/']({name: 'foo'}, { path: '/name' }, {}).length).toBe(1); + }); + + it('should ignore properties that don\'t exist a list of child errors', () => { + expect(validators['/']({ foo: 'bar' }, { path: '/foo' }, {})).toBeUndefined(); + }); + }); + }); +}); diff --git a/ui/src/app/metadata/provider/model/base.provider.form.ts b/ui/src/app/metadata/provider/model/base.provider.form.ts new file mode 100644 index 000000000..a542ebb47 --- /dev/null +++ b/ui/src/app/metadata/provider/model/base.provider.form.ts @@ -0,0 +1,60 @@ +import { Wizard } from '../../../wizard/model'; +import { BaseMetadataProvider } from '../../domain/model/providers'; + +export const BaseMetadataProviderEditor: Wizard = { + label: 'BaseMetadataProvider', + type: 'BaseMetadataResolver', + getValidators(namesList: string[]): any { + const validators = { + '/': (value, property, form_current) => { + let errors; + // iterate all customer + Object.keys(value).forEach((key) => { + const item = value[key]; + const validatorKey = `/${key}`; + const validator = validators.hasOwnProperty(validatorKey) ? validators[validatorKey] : null; + const error = validator ? validator(item, { path: `/${key}` }, form_current) : null; + if (error) { + errors = errors || []; + errors.push(error); + } + }); + return errors; + }, + '/name': (value, property, form) => { + const err = namesList.indexOf(value) > -1 ? { + code: 'INVALID_NAME', + path: `#${property.path}`, + message: 'Name must be unique.', + params: [value] + } : null; + return err; + } + }; + return validators; + }, + translate: { + parser: (changes: any): BaseMetadataProvider => changes.metadataFilters ? ({ + ...changes, + metadataFilters: [ + ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([ + ...collection, + { + ...changes.metadataFilters[filterName], + '@type': filterName + } + ]), []) + ] + }) : changes, + formatter: (changes: BaseMetadataProvider): any => changes.metadataFilters ? ({ + ...changes, + metadataFilters: { + ...(changes.metadataFilters || []).reduce((collection, filter) => ({ + ...collection, + [filter['@type']]: filter + }), {}) + } + }) : changes + }, + steps: [] +}; diff --git a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts index 41723be8f..5af8642bb 100644 --- a/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts +++ b/ui/src/app/metadata/provider/model/file-backed-http.provider.form.ts @@ -1,32 +1,11 @@ import { Wizard } from '../../../wizard/model'; import { FileBackedHttpMetadataProvider } from '../../domain/model/providers/file-backed-http-metadata-provider'; +import { BaseMetadataProviderEditor } from './base.provider.form'; export const FileBackedHttpMetadataProviderWizard: Wizard = { + ...BaseMetadataProviderEditor, label: 'FileBackedHttpMetadataProvider', type: 'FileBackedHttpMetadataResolver', - translate: { - parser: (changes: any): FileBackedHttpMetadataProvider => changes.metadataFilters ? ({ - ...changes, - metadataFilters: [ - ...Object.keys(changes.metadataFilters).reduce((collection, filterName) => ([ - ...collection, - { - ...changes.metadataFilters[filterName], - '@type': filterName - } - ]), []) - ] - }) : changes, - formatter: (changes: FileBackedHttpMetadataProvider): any => changes.metadataFilters ? ({ - ...changes, - metadataFilters: { - ...(changes.metadataFilters || []).reduce((collection, filter) => ({ - ...collection, - [filter['@type']]: filter - }), {}) - } - }) : changes - }, steps: [ { id: 'common', @@ -60,3 +39,45 @@ export const FileBackedHttpMetadataProviderWizard: Wizard = { + ...FileBackedHttpMetadataProviderWizard, + steps: [ + { + id: 'filter-list', + label: 'Filter List', + index: 0 + }, + { + id: 'common', + label: 'Common Attributes', + index: 1, + initialValues: [], + schema: 'assets/schema/provider/filebacked-http-common.editor.schema.json' + }, + { + id: 'reloading', + label: 'Reloading Attributes', + index: 2, + initialValues: [], + schema: 'assets/schema/provider/filebacked-http-reloading.schema.json' + }, + { + id: 'filters', + label: 'Metadata Filter Plugins', + index: 3, + initialValues: [ + { key: 'metadataFilters', value: [] } + ], + schema: 'assets/schema/provider/filebacked-http-filters.schema.json' + }, + { + id: 'advanced', + label: 'Advanced Settings', + index: 4, + initialValues: [], + schema: 'assets/schema/provider/filebacked-http-advanced.schema.json' + } + ] +}; diff --git a/ui/src/app/metadata/provider/model/index.ts b/ui/src/app/metadata/provider/model/index.ts index 508650ca7..a6c438df4 100644 --- a/ui/src/app/metadata/provider/model/index.ts +++ b/ui/src/app/metadata/provider/model/index.ts @@ -1,8 +1,15 @@ import { FileBackedHttpMetadataProviderWizard } from './file-backed-http.provider.form'; +import { FileBackedHttpMetadataProviderEditor } from './file-backed-http.provider.form'; +import { BaseMetadataProviderEditor } from './base.provider.form'; -export const MetadataProviderTypes = [ +export const MetadataProviderWizardTypes = [ FileBackedHttpMetadataProviderWizard ]; +export const MetadataProviderEditorTypes = [ + FileBackedHttpMetadataProviderEditor, + BaseMetadataProviderEditor +]; + export * from './file-backed-http.provider.form'; export * from './provider.form'; diff --git a/ui/src/app/metadata/provider/model/provider.form.ts b/ui/src/app/metadata/provider/model/provider.form.ts index 698d20134..f7e0ae773 100644 --- a/ui/src/app/metadata/provider/model/provider.form.ts +++ b/ui/src/app/metadata/provider/model/provider.form.ts @@ -1,14 +1,12 @@ import { Wizard, WizardStep } from '../../../wizard/model'; import { MetadataProvider } from '../../domain/model'; import { Metadata } from '../../domain/domain.type'; +import { BaseMetadataProviderEditor } from './base.provider.form'; export const MetadataProviderWizard: Wizard = { + ...BaseMetadataProviderEditor, label: 'MetadataProvider', type: 'MetadataProvider', - translate: { - parser: changes => changes, - formatter: model => model - }, steps: [ { id: 'new', diff --git a/ui/src/app/metadata/provider/provider.module.ts b/ui/src/app/metadata/provider/provider.module.ts index 104bae13d..288adf30b 100644 --- a/ui/src/app/metadata/provider/provider.module.ts +++ b/ui/src/app/metadata/provider/provider.module.ts @@ -19,7 +19,12 @@ import { CustomWidgetRegistry } from '../../schema-form/registry'; import { SummaryPropertyComponent } from './component/summary-property.component'; import { CollectionEffects } from './effect/collection.effect'; import { SharedModule } from '../../shared/shared.module'; - +import { ProviderEditComponent } from './container/provider-edit.component'; +import { ProviderSelectComponent } from './container/provider-select.component'; +import { ProviderEditStepComponent } from './container/provider-edit-step.component'; +import { EntityEffects } from './effect/entity.effect'; +import { ProviderFilterListComponent } from './container/provider-filter-list.component'; +import { NgbDropdownModule } from '../../../../node_modules/@ng-bootstrap/ng-bootstrap'; @NgModule({ declarations: [ @@ -27,6 +32,10 @@ import { SharedModule } from '../../shared/shared.module'; ProviderWizardComponent, ProviderWizardStepComponent, ProviderWizardSummaryComponent, + ProviderEditComponent, + ProviderEditStepComponent, + ProviderSelectComponent, + ProviderFilterListComponent, SummaryPropertyComponent ], entryComponents: [], @@ -36,7 +45,8 @@ import { SharedModule } from '../../shared/shared.module'; WizardModule, RouterModule, SharedModule, - FormModule + FormModule, + NgbDropdownModule ], exports: [] }) @@ -55,7 +65,7 @@ export class ProviderModule { imports: [ ProviderModule, StoreModule.forFeature('provider', fromProvider.reducers), - EffectsModule.forFeature([EditorEffects, CollectionEffects]) + EffectsModule.forFeature([EntityEffects, EditorEffects, CollectionEffects]) ] }) export class RootProviderModule { } diff --git a/ui/src/app/metadata/provider/provider.routing.ts b/ui/src/app/metadata/provider/provider.routing.ts index 98cdddd85..cf491a92f 100644 --- a/ui/src/app/metadata/provider/provider.routing.ts +++ b/ui/src/app/metadata/provider/provider.routing.ts @@ -3,6 +3,10 @@ import { Routes } from '@angular/router'; import { ProviderComponent } from './container/provider.component'; import { ProviderWizardComponent } from './container/provider-wizard.component'; import { ProviderWizardStepComponent } from './container/provider-wizard-step.component'; +import { ProviderEditComponent } from './container/provider-edit.component'; +import { ProviderEditStepComponent } from './container/provider-edit-step.component'; +import { ProviderSelectComponent } from './container/provider-select.component'; +import { ProviderFilterListComponent } from './container/provider-filter-list.component'; export const ProviderRoutes: Routes = [ { @@ -11,8 +15,7 @@ export const ProviderRoutes: Routes = [ children: [ { path: 'wizard', - redirectTo: `wizard/new`, - pathMatch: 'prefix' + redirectTo: `wizard/new` }, { path: 'wizard', @@ -24,6 +27,26 @@ export const ProviderRoutes: Routes = [ component: ProviderWizardStepComponent } ] + }, + { + path: ':providerId', + component: ProviderSelectComponent, + children: [ + { + path: 'edit', + component: ProviderEditComponent, + children: [ + { + path: 'filter-list', + component: ProviderFilterListComponent + }, + { + path: ':form', + component: ProviderEditStepComponent + } + ] + } + ] } ] } diff --git a/ui/src/app/metadata/provider/reducer/collection.reducer.ts b/ui/src/app/metadata/provider/reducer/collection.reducer.ts index 6bc1af8e7..8cc0c6f0c 100644 --- a/ui/src/app/metadata/provider/reducer/collection.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/collection.reducer.ts @@ -23,6 +23,13 @@ export const initialState: CollectionState = adapter.getInitialState({ export function reducer(state = initialState, action: ProviderCollectionActionsUnion): CollectionState { switch (action.type) { + case ProviderCollectionActionTypes.SELECT_PROVIDER_SUCCESS: { + return adapter.upsertOne(action.payload, { + ...state, + selectedProviderId: action.payload.id.toString() + }); + } + case ProviderCollectionActionTypes.LOAD_PROVIDER_SUCCESS: { return adapter.addAll(action.payload, { ...state, diff --git a/ui/src/app/metadata/provider/reducer/entity.reducer.ts b/ui/src/app/metadata/provider/reducer/entity.reducer.ts index 5aa7eceb1..d5b63eca1 100644 --- a/ui/src/app/metadata/provider/reducer/entity.reducer.ts +++ b/ui/src/app/metadata/provider/reducer/entity.reducer.ts @@ -3,13 +3,11 @@ import { EntityActionTypes, EntityActionUnion } from '../action/entity.action'; export interface EntityState { saving: boolean; - base: MetadataProvider; changes: MetadataProvider; } export const initialState: EntityState = { saving: false, - base: null, changes: null }; @@ -28,14 +26,6 @@ export function reducer(state = initialState, action: EntityActionUnion): Entity } }; } - case EntityActionTypes.SELECT_PROVIDER: { - return { - ...state, - base: { - ...action.payload - } - }; - } case EntityActionTypes.UPDATE_PROVIDER: { return { ...state, @@ -54,4 +44,4 @@ export function reducer(state = initialState, action: EntityActionUnion): Entity export const isEntitySaved = (state: EntityState) => !Object.keys(state.changes).length && !state.saving; export const getEntityChanges = (state: EntityState) => state.changes; export const isEditorSaving = (state: EntityState) => state.saving; -export const getUpdatedEntity = (state: EntityState) => ({ ...state.base, ...state.changes }); +export const getUpdatedEntity = (state: EntityState) => state.changes; diff --git a/ui/src/app/wizard/model/wizard.ts b/ui/src/app/wizard/model/wizard.ts index 8b5fe344e..3c9477f5a 100644 --- a/ui/src/app/wizard/model/wizard.ts +++ b/ui/src/app/wizard/model/wizard.ts @@ -6,13 +6,15 @@ export interface Wizard { parser(changes: Partial, schema?: any), formatter(changes: Partial, schema?: any) }; + + getValidators?(params: any): { [key: string]: any }; } export interface WizardStep { id: string; label: string; initialValues?: WizardValue[]; - schema: string; + schema?: string; index: number; } diff --git a/ui/src/app/wizard/reducer/index.spec.ts b/ui/src/app/wizard/reducer/index.spec.ts index 78ff09441..9c8b246f9 100644 --- a/ui/src/app/wizard/reducer/index.spec.ts +++ b/ui/src/app/wizard/reducer/index.spec.ts @@ -84,7 +84,6 @@ describe('wizard index selectors', () => { describe('getModelFn method', () => { it('should return the model', () => { const step = FileBackedHttpMetadataProviderWizard.steps.find(s => s.id === 'filters'); - console.log(step); expect(selectors.getModelFn(step)).toEqual({ metadataFilters: [] }); }); }); diff --git a/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json b/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json new file mode 100644 index 000000000..601aae3db --- /dev/null +++ b/ui/src/assets/schema/provider/filebacked-http-advanced.schema.json @@ -0,0 +1,6 @@ +{ + "type": "object", + "title": "Advanced Settings", + "order": [], + "properties": {} +} diff --git a/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json new file mode 100644 index 000000000..ce53531bf --- /dev/null +++ b/ui/src/assets/schema/provider/filebacked-http-common.editor.schema.json @@ -0,0 +1,244 @@ +{ + "type": "object", + "order": [ + "name", + "@type", + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ], + "required": [ + "xmlId", + "metadataURL" + ], + "anyOf": [ + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + true + ] + } + }, + "required": [ + "backingFile" + ] + }, + { + "properties": { + "initializeFromBackupFile": { + "enum": [ + false + ] + } + } + } + ], + "fieldsets": [ + { + "type": "section", + "fields": [ + "name", + "@type" + ] + }, + { + "fields": [ + "xmlId", + "metadataURL", + "initializeFromBackupFile", + "backingFile", + "backupFileInitNextRefreshDelay", + "requireValidMetadata", + "failFastInitialization", + "useDefaultPredicateRegistry", + "satisfyAnyPredicates" + ] + } + ], + "properties": { + "name": { + "title": "Metadata Provider Name (Dashboard Display Only)", + "description": "Metadata Provider Name", + "type": "string", + "widget": { + "id": "string", + "help": "Must be unique." + } + }, + "@type": { + "title": "Metadata Provider Type", + "description": "Metadata Provider Type", + "placeholder": "Select a metadata provider type", + "type": "string", + "readOnly": true, + "widget": { + "id": "select" + }, + "oneOf": [ + { + "enum": [ + "FileBackedHttpMetadataResolver" + ], + "description": "FileBackedHttpMetadataProvider" + } + ] + }, + "xmlId": { + "title": "ID", + "description": "Identifier for logging, identification for command line reload, etc.", + "type": "string", + "default": "", + "minLength": 1 + }, + "metadataURL": { + "title": "Metadata URL", + "description": "Identifier for logging, identification for command line reload, etc.", + "type": "string", + "default": "", + "minLength": 1 + }, + "initializeFromBackupFile": { + "title": "Initialize From Backup File?", + "description": "Flag indicating whether initialization should first attempt to load metadata from the backup file. If true, foreground initialization will be performed by loading the backing file, and then a refresh from the remote HTTP server will be scheduled to execute in a background thread, after a configured delay. This can improve IdP startup times when the remote HTTP file is large in size.", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": true + }, + "backingFile": { + "title": "Backing File", + "description": "Specifies where the backing file is located. If the remote server is unavailable at startup, the backing file is loaded instead.", + "type": "string", + "default": "", + "visibleIf": { + "initializeFromBackupFile": [ + true + ] + } + }, + "backupFileInitNextRefreshDelay": { + "title": "Backup File Init Next Refresh Delay", + "description": "Delay duration after which to schedule next HTTP refresh when initialized from the backing file.", + "type": "string", + "visibleIf": { + "initializeFromBackupFile": [ + true + ] + } + }, + "requireValidMetadata": { + "title": "Require Valid Metadata?", + "description": "Whether candidate metadata found by the resolver must be valid in order to be returned (where validity is implementation specific, but in SAML cases generally depends on a validUntil attribute.) If this flag is true, then invalid candidate metadata will not be returned.", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": true + }, + "failFastInitialization": { + "title": "Fail Fast Initialization?", + "description": "Whether to fail initialization of the underlying MetadataResolverService (and possibly the IdP as a whole) if the initialization of a metadata provider fails. When false, the IdP may start, and will continue to attempt to reload valid metadata if configured to do so, but operations that require valid metadata will fail until it does.", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": true + }, + "useDefaultPredicateRegistry": { + "title": "Use Default Predicate Registry?", + "description": "Whether to fail initialization of the underlying MetadataResolverService (and possibly the IdP as a whole) if the initialization of a metadata provider fails. When false, the IdP may start, and will continue to attempt to reload valid metadata if configured to do so, but operations that require valid metadata will fail until it does.", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": true + }, + "satisfyAnyPredicates": { + "title": "Satisy Any Predicates?", + "description": "Flag which determines whether predicates used in filtering are connected by a logical 'OR' (true) or by logical 'AND' (false).", + "type": "boolean", + "widget": { + "id": "boolean-radio" + }, + "oneOf": [ + { + "enum": [ + true + ], + "description": "True" + }, + { + "enum": [ + false + ], + "description": "False" + } + ], + "default": false + } + } +} \ No newline at end of file diff --git a/ui/src/assets/schema/provider/filebacked-http-common.schema.json b/ui/src/assets/schema/provider/filebacked-http-common.schema.json index 65c919add..73414de00 100644 --- a/ui/src/assets/schema/provider/filebacked-http-common.schema.json +++ b/ui/src/assets/schema/provider/filebacked-http-common.schema.json @@ -1,7 +1,7 @@ { "type": "object", "order": [ - "id", + "xmlId", "metadataURL", "initializeFromBackupFile", "backingFile", @@ -11,7 +11,7 @@ "useDefaultPredicateRegistry", "satisfyAnyPredicates" ], - "required": ["id", "metadataURL"], + "required": ["xmlId", "metadataURL"], "dependencies": { "initializeFromBackupFile": {"required": ["backingFile", "backupFileInitNextRefreshDelay"]} }, @@ -40,7 +40,7 @@ } ], "properties": { - "id": { + "xmlId": { "title": "ID", "description": "Identifier for logging, identification for command line reload, etc.", "type": "string", diff --git a/ui/src/testing/activated-route.stub.ts b/ui/src/testing/activated-route.stub.ts index 511b78744..f41853bf9 100644 --- a/ui/src/testing/activated-route.stub.ts +++ b/ui/src/testing/activated-route.stub.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { convertToParamMap, ParamMap } from '@angular/router'; +import { convertToParamMap, ParamMap, ActivatedRoute } from '@angular/router'; @Injectable() export class ActivatedRouteStub { @@ -13,6 +13,9 @@ export class ActivatedRouteStub { // Test parameters private _testParamMap: ParamMap; + + private _firstChild: ActivatedRouteStub; + get testParamMap() { return this._testParamMap; } set testParamMap(params: {}) { this._testParamMap = convertToParamMap(params); @@ -27,4 +30,12 @@ export class ActivatedRouteStub { get params() { return this.paramMap; } + + get firstChild(): ActivatedRouteStub { + return this._firstChild; + } + + set firstChild(stub: ActivatedRouteStub) { + this._firstChild = stub; + } }