From 9a6cda8e74416568669f9db33fe14a298777c8a1 Mon Sep 17 00:00:00 2001 From: Ryan Mathis Date: Thu, 26 Apr 2018 15:41:41 -0700 Subject: [PATCH 1/3] SHIBUI-457: Fixed responsive issue with button sizing on edit filter page --- .../app/metadata-filter/container/edit-filter.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/app/metadata-filter/container/edit-filter.component.html b/ui/src/app/metadata-filter/container/edit-filter.component.html index 187420e70..cfe493c38 100644 --- a/ui/src/app/metadata-filter/container/edit-filter.component.html +++ b/ui/src/app/metadata-filter/container/edit-filter.component.html @@ -60,7 +60,7 @@
-
+
- - +
+ + + + Minimum 4 characters. + + + + Entity ID is required + Entity ID not found + + +
- - - Entity ID is required - Entity ID not found - -
@@ -99,11 +104,24 @@ Preview XML - - diff --git a/ui/src/app/metadata-filter/container/edit-filter.component.ts b/ui/src/app/metadata-filter/container/edit-filter.component.ts index 8a7e9f0cf..f3bc80385 100644 --- a/ui/src/app/metadata-filter/container/edit-filter.component.ts +++ b/ui/src/app/metadata-filter/container/edit-filter.component.ts @@ -47,6 +47,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { loading$: Observable; processing$: Observable; preview$: Observable; + isSaving$: Observable; form: FormGroup = this.fb.group({ entityId: ['', [Validators.required]], @@ -55,6 +56,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { }); filter: MetadataFilter; + filterEntity: Filter; isValid = false; @@ -75,6 +77,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { this.loading$ = this.store.select(fromFilter.getIsLoading); this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); this.preview$ = this.store.select(fromFilter.getPreview); + this.isSaving$ = this.store.select(fromFilter.getSaving); this.entityIds$.subscribe(ids => this.ids = ids); @@ -86,6 +89,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { filterEnabled }); this.filter = filter; + this.filterEntity = new Filter(filter); this.store.dispatch(new SelectId(entityId)); }); @@ -139,7 +143,7 @@ export class EditFilterComponent implements OnInit, OnDestroy { const input = this.form.get('entityId'); input.disable(); input.clearAsyncValidators(); - input.reset(this.filter.entityId); + input.setValue(this.filterEntity.entityId); } searchEntityIds(term: string): void { diff --git a/ui/src/app/metadata-filter/container/new-filter.component.html b/ui/src/app/metadata-filter/container/new-filter.component.html index dec981eaf..bae6ea5e2 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.html +++ b/ui/src/app/metadata-filter/container/new-filter.component.html @@ -60,6 +60,9 @@ + + Minimum 4 characters. + Entity ID is required @@ -70,11 +73,22 @@
- -
diff --git a/ui/src/app/metadata-filter/container/new-filter.component.ts b/ui/src/app/metadata-filter/container/new-filter.component.ts index 5b67a08b9..f83ad5efd 100644 --- a/ui/src/app/metadata-filter/container/new-filter.component.ts +++ b/ui/src/app/metadata-filter/container/new-filter.component.ts @@ -54,6 +54,7 @@ export class NewFilterComponent implements OnInit, OnDestroy { loading$: Observable; processing$: Observable; preview$: Observable; + isSaving$: Observable; form: FormGroup = this.fb.group({ entityId: ['', [Validators.required], [ @@ -79,6 +80,7 @@ export class NewFilterComponent implements OnInit, OnDestroy { this.loading$ = this.store.select(fromFilter.getIsLoading); this.processing$ = this.loading$.withLatestFrom(this.showMore$, (l, s) => !s && l); this.preview$ = this.store.select(fromFilter.getPreview); + this.isSaving$ = this.store.select(fromFilter.getSaving); this.entityIds$.subscribe(ids => this.ids = ids); } diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts index f482928c5..2b42917c2 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.spec.ts @@ -5,7 +5,8 @@ import * as actions from '../action/filter.action'; const snapshot: fromFilter.FilterState = { selected: null, changes: null, - preview: null + preview: null, + saving: false }; describe('Filter Reducer', () => { diff --git a/ui/src/app/metadata-filter/reducer/filter.reducer.ts b/ui/src/app/metadata-filter/reducer/filter.reducer.ts index 86f2e04b7..4b36bff38 100644 --- a/ui/src/app/metadata-filter/reducer/filter.reducer.ts +++ b/ui/src/app/metadata-filter/reducer/filter.reducer.ts @@ -9,12 +9,14 @@ export interface FilterState { selected: string | null; changes: MetadataFilter | null; preview: MDUI | null; + saving: boolean; } export const initialState: FilterState = { selected: null, changes: null, - preview: null + preview: null, + saving: false }; export function reducer(state = initialState, action: filter.Actions | FilterCollectionActionsUnion): FilterState { @@ -46,6 +48,13 @@ export function reducer(state = initialState, action: filter.Actions | FilterCol } }; } + case FilterCollectionActionTypes.ADD_FILTER: + case FilterCollectionActionTypes.UPDATE_FILTER_REQUEST: { + return { + ...initialState, + saving: true + }; + } case FilterCollectionActionTypes.ADD_FILTER_SUCCESS: case FilterCollectionActionTypes.UPDATE_FILTER_SUCCESS: case filter.CANCEL_CREATE_FILTER: { @@ -62,3 +71,4 @@ export function reducer(state = initialState, action: filter.Actions | FilterCol export const getSelected = (state: FilterState) => state.selected; export const getFilterChanges = (state: FilterState) => state.changes; export const getPreview = (state: FilterState) => state.preview; +export const getSaving = (state: FilterState) => state.saving; diff --git a/ui/src/app/metadata-filter/reducer/index.ts b/ui/src/app/metadata-filter/reducer/index.ts index 73b342ff2..f94f5d385 100644 --- a/ui/src/app/metadata-filter/reducer/index.ts +++ b/ui/src/app/metadata-filter/reducer/index.ts @@ -25,6 +25,7 @@ export const getFilterFromState = createSelector(getFilterState, getFiltersFromS export const getSelected = createSelector(getFilterFromState, fromFilter.getSelected); export const getFilter = createSelector(getFilterFromState, fromFilter.getFilterChanges); export const getPreview = createSelector(getFilterFromState, fromFilter.getPreview); +export const getSaving = createSelector(getFilterFromState, fromFilter.getSaving); export const getSearchFromState = createSelector(getFilterState, getSearchFromStateFn); export const getEntityCollection = createSelector(getSearchFromState, fromSearch.getEntityIds); diff --git a/ui/src/app/shared/autocomplete/autocomplete.component.scss b/ui/src/app/shared/autocomplete/autocomplete.component.scss index a491882aa..192e30310 100644 --- a/ui/src/app/shared/autocomplete/autocomplete.component.scss +++ b/ui/src/app/shared/autocomplete/autocomplete.component.scss @@ -1,3 +1,8 @@ +.dropdown.form-group { + margin-bottom: 0px; +} + .dropdown-menu { - width: 100%; + min-width: 100%; + width: auto; } \ No newline at end of file diff --git a/ui/src/locale/en.xlf b/ui/src/locale/en.xlf index 4ab25f300..21ced3e8e 100644 --- a/ui/src/locale/en.xlf +++ b/ui/src/locale/en.xlf @@ -2103,6 +2103,14 @@ 93 + + Minimum 4 characters. + Minimum 4 characters. + + app/metadata-filter/container/new-filter.component.ts + 93 + + Entity Preview Entity Preview diff --git a/ui/src/locale/es.xlf b/ui/src/locale/es.xlf index cb3d21da3..d9001cead 100644 --- a/ui/src/locale/es.xlf +++ b/ui/src/locale/es.xlf @@ -2071,6 +2071,14 @@ 93 + + Minimum 4 characters. + Minimum 4 characters. (es) + + app/metadata-filter/container/new-filter.component.ts + 93 + + Entity Preview Entity Preview From 85e508bbb6d296b0a428b6b61e4630ced77fb2eb Mon Sep 17 00:00:00 2001 From: Bill Smith Date: Fri, 27 Apr 2018 16:30:29 +0000 Subject: [PATCH 3/3] Merged in SHIBUI-456 (pull request #63) SHIBUI-456 * [SHIBUI-456] Fix tomcat configuration for `/entities` endpoint update documentation * [SHIBUI-456] Fix tomcat configuration for `/entities` endpoint * [SHIBUI-456] Copied tests from EntityControllerTests and modified to work with WebTestClient (and a live Tomcat). --- README.md | 11 ++ .../ui/configuration/TomcatConfiguration.java | 13 ++ .../EntitiesControllerIntegrationTests.groovy | 135 +++++++++++++++--- 3 files changed, 143 insertions(+), 16 deletions(-) create mode 100644 backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/TomcatConfiguration.java diff --git a/README.md b/README.md index 4033766c8..a7f5b9994 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,17 @@ allowed with: -Dorg.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH=true ``` +In Apache HTTPD, you'll need something like: + +``` + + AllowEncodedSlashes NoDecode + ServerName shibui.unicon.net + ProxyPass / http://localhost:8080/ nocanon + ProxyPassReverse / http://localhost:8080/ + +``` + ### Running as an executable `java -jar shibui.war` diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/TomcatConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/TomcatConfiguration.java new file mode 100644 index 000000000..e3c2d019c --- /dev/null +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/TomcatConfiguration.java @@ -0,0 +1,13 @@ +package edu.internet2.tier.shibboleth.admin.ui.configuration; + +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.stereotype.Component; + +@Component +public class TomcatConfiguration implements WebServerFactoryCustomizer { + @Override + public void customize(ConfigurableServletWebServerFactory factory) { + System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true"); + } +} diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy index 62a15c646..2ed405784 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntitiesControllerIntegrationTests.groovy @@ -1,22 +1,41 @@ package edu.internet2.tier.shibboleth.admin.ui.controller +import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects +import net.shibboleth.ext.spring.resource.ResourceHelper +import org.joda.time.DateTime +import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver +import org.opensaml.saml.metadata.resolver.MetadataResolver +import org.opensaml.saml.metadata.resolver.filter.MetadataFilterChain +import org.opensaml.saml.metadata.resolver.impl.ResourceBackedMetadataResolver import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.core.io.ClassPathResource import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.util.DefaultUriBuilderFactory +import org.xmlunit.builder.DiffBuilder +import org.xmlunit.builder.Input +import org.xmlunit.diff.DefaultNodeMatcher +import org.xmlunit.diff.ElementSelectors import spock.lang.Specification /** * @author Bill Smith (wsmith@unicon.net) */ -// TODO: implement -// @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -// @ActiveProfiles("no-auth") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("no-auth") class EntitiesControllerIntegrationTests extends Specification { @Autowired private WebTestClient webClient + def setup() { + // yeah, don't ask... this is just shenanigans + this.webClient.webClient.uriBuilderFactory.encodingMode = DefaultUriBuilderFactory.EncodingMode.NONE + } + def "GET /api/entities returns the proper json"() { given: def expectedBody = ''' @@ -55,24 +74,108 @@ class EntitiesControllerIntegrationTests extends Specification { ''' when: - def x = 1 - // TODO: implement - /* def result = this.webClient .get() .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") - .exchange() - */ + .exchange() // someday, I'd like to know why IntelliJ "cannot resolve symbol 'exchange'" then: - assert 1 - // TODO: implement - /* result.expectStatus().isOk() - .expectBody().consumeWith( - { response -> new String(response.getResponseBody()) == expectedBody } - ) - //.expectedBody() // some other json comparison - */ + .expectBody() + .json(expectedBody) + } + + def "GET /api/entities/test is not found"() { + when: + def result = this.webClient + .get() + .uri("/api/entities/test") + .exchange() + + then: + result.expectStatus().isNotFound() + } + + def "GET /api/entities/test XML is not found"() { + when: + def result = this.webClient + .get() + .uri("/api/entities/test") + .header('Accept', 'application/xml') + .exchange() + + then: + result.expectStatus().isNotFound() + } + + def "GET /api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1 XML returns proper XML"() { + given: + def expectedBody = ''' + + + + + internal + + + givenName + employeeNumber + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified + + + +''' + when: + def result = this.webClient + .get() + .uri("/api/entities/http%3A%2F%2Ftest.scaldingspoon.org%2Ftest1") + .header('Accept', 'application/xml') + .exchange() + + then: + def resultBody = result.expectStatus().isOk() + //.expectHeader().contentType("application/xml;charset=ISO-8859-1") // should this really be ISO-8859-1? + // expectedBody encoding is UTF-8... + .expectHeader().contentType("application/xml;charset=UTF-8") + .expectBody(String.class) + .returnResult() + def diff = DiffBuilder.compare(Input.fromString(expectedBody)).withTest(Input.fromString(resultBody.getResponseBody())).ignoreComments().checkForSimilar().ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)).build() + !diff.hasDifferences() + } + + @TestConfiguration + static class Config { + @Autowired + OpenSamlObjects openSamlObjects + + @Bean + MetadataResolver metadataResolver() { + def resource = ResourceHelper.of(new ClassPathResource("/metadata/aggregate.xml")) + def aggregate = new ResourceBackedMetadataResolver(resource){ + @Override + DateTime getLastRefresh() { + return null + } + } + + aggregate.with { + it.metadataFilter = new MetadataFilterChain() + it.id = 'testme' + it.parserPool = openSamlObjects.parserPool + it.initialize() + it + } + + return new ChainingMetadataResolver().with { + it.id = 'chain' + it.resolvers = [aggregate] + it.initialize() + it + } + } } }