diff --git a/components/nexus-cleanup/src/main/java/org/sonatype/nexus/cleanup/internal/rest/CleanupPolicyResource.java b/components/nexus-cleanup/src/main/java/org/sonatype/nexus/cleanup/internal/rest/CleanupPolicyResource.java index cf487c2415..82baaa758d 100644 --- a/components/nexus-cleanup/src/main/java/org/sonatype/nexus/cleanup/internal/rest/CleanupPolicyResource.java +++ b/components/nexus-cleanup/src/main/java/org/sonatype/nexus/cleanup/internal/rest/CleanupPolicyResource.java @@ -111,7 +111,7 @@ public CleanupPolicyResource( this.cleanupPolicyStorage = checkNotNull(cleanupPolicyStorage); this.formats = checkNotNull(formats); this.formatNames = formats.stream().map(Format::getValue).collect(Collectors.toList()); - this.formatNames.add("*"); + this.formatNames.add(ALL_FORMATS); this.cleanupFormatConfigurationMap = checkNotNull(cleanupFormatConfigurationMap); this.defaultCleanupFormatConfiguration = checkNotNull(cleanupFormatConfigurationMap.get("default")); this.cleanupPreviewHelper = checkNotNull(cleanupPreviewHelper); @@ -175,12 +175,18 @@ public CleanupPolicyXO edit( throw new ValidationErrorsException("format", "specified format " + cleanupPolicyXO.getFormat() + " is not valid."); } + int inUseCount = (int) repositoryManager.browseForCleanupPolicy(name).count(); + if (!cleanupPolicyXO.getFormat().equals(ALL_FORMATS) && + !cleanupPolicy.getFormat().equals(cleanupPolicyXO.getFormat()) && + inUseCount > 0) { + throw new ValidationErrorsException("format", "You cannot change the format of a policy that is in use."); + } + cleanupPolicy.setNotes(cleanupPolicyXO.getNotes()); cleanupPolicy.setFormat(cleanupPolicyXO.getFormat()); cleanupPolicy.setCriteria(toCriteriaMap(cleanupPolicyXO)); - return CleanupPolicyXO.fromCleanupPolicy(cleanupPolicyStorage.update(cleanupPolicy), - (int) repositoryManager.browseForCleanupPolicy(name).count()); + return CleanupPolicyXO.fromCleanupPolicy(cleanupPolicyStorage.update(cleanupPolicy), inUseCount); } @DELETE diff --git a/components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImpl.java b/components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImpl.java index 02b1ff399f..84bb843dae 100644 --- a/components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImpl.java +++ b/components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImpl.java @@ -14,6 +14,7 @@ import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.inject.Inject; @@ -21,7 +22,7 @@ import javax.inject.Singleton; import org.sonatype.goodies.common.ComponentSupport; -import org.sonatype.nexus.selector.CselSelector; +import org.sonatype.nexus.selector.JexlSelector; import org.sonatype.nexus.selector.SelectorConfiguration; import org.sonatype.nexus.selector.SelectorEvaluationException; import org.sonatype.nexus.selector.SelectorFilterBuilder; @@ -52,13 +53,16 @@ public String buildFilter( final List selectors, final Map filterParameters) { - if (selectors.isEmpty()) { + List activeSelectors = + selectors.stream().filter(s -> !JexlSelector.TYPE.equals(s.getType())).collect(Collectors.toList()); + + if (activeSelectors.isEmpty()) { return null; } StringBuilder filterBuilder = new StringBuilder(); - if (selectors.size() > 1) { + if (activeSelectors.size() > 1) { filterBuilder.append('('); } @@ -68,9 +72,9 @@ public String buildFilter( .parameterPrefix("#{" + FILTER_PARAMS + ".") .parameterSuffix("}"); - appendSelectors(filterBuilder, sqlBuilder, selectors, filterParameters); + appendSelectors(filterBuilder, sqlBuilder, activeSelectors, filterParameters); - if (selectors.size() > 1) { + if (activeSelectors.size() > 1) { filterBuilder.append(')'); } @@ -86,27 +90,25 @@ public void appendSelectors( int selectorCount = 0; for (SelectorConfiguration selector : selectors) { - if (CselSelector.TYPE.equals(selector.getType())) { - try { - sqlBuilder.parameterNamePrefix("s" + selectorCount + "p"); + try { + sqlBuilder.parameterNamePrefix("s" + selectorCount + "p"); - selectorManager.toSql(selector, sqlBuilder); + selectorManager.toSql(selector, sqlBuilder); - if (selectorCount > 0) { - filterBuilder.append(" or "); - } + if (selectorCount > 0) { + filterBuilder.append(" or "); + } - filterBuilder.append('(').append(sqlBuilder.getQueryString()).append(')'); - filterParameters.putAll(sqlBuilder.getQueryParameters()); + filterBuilder.append('(').append(sqlBuilder.getQueryString()).append(')'); + filterParameters.putAll(sqlBuilder.getQueryParameters()); - selectorCount++; - } - catch (SelectorEvaluationException e) { - log.warn("Problem evaluating selector {} as SQL", selector.getName(), e); - } - finally { - sqlBuilder.clearQueryString(); - } + selectorCount++; + } + catch (SelectorEvaluationException e) { + log.warn("Problem evaluating selector {} as SQL", selector.getName(), log.isDebugEnabled() ? e : null); + } + finally { + sqlBuilder.clearQueryString(); } } } diff --git a/components/nexus-core/src/test/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImplTest.java b/components/nexus-core/src/test/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImplTest.java index 20740fc8a5..6e2d474b59 100644 --- a/components/nexus-core/src/test/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImplTest.java +++ b/components/nexus-core/src/test/java/org/sonatype/nexus/internal/selector/SelectorFilterBuilderImplTest.java @@ -142,7 +142,21 @@ public void testJexl() { Map filterParameters = new HashMap<>(); String filter = underTest.buildFilter("raw", "request_path", ImmutableList.of(selector), filterParameters); - assertThat(filter, is("")); + assertThat(filter, nullValue()); + assertThat(filterParameters.size(), is(0)); + } + + // Jexl expressions are not put into the filter, they need to be checked in code + @Test + public void testMultipleJexl() { + SelectorConfiguration selector1 = createSelectorConfiguration(JexlSelector.TYPE, "selector1", "path.size() > 5"); + SelectorConfiguration selector2 = createSelectorConfiguration(JexlSelector.TYPE, "selector2", "format == 'raw'"); + + Map filterParameters = new HashMap<>(); + String filter = + underTest.buildFilter("raw", "request_path", ImmutableList.of(selector1, selector2), filterParameters); + + assertThat(filter, nullValue()); assertThat(filterParameters.size(), is(0)); } @@ -156,8 +170,7 @@ public void testMixedTypes() { String filter = underTest.buildFilter("raw", "request_path", ImmutableList.of(selector1, selector2), filterParameters); - // appends extra parenthesis because it expected more than one expression - assertThat(filter, is("((request_path = #{filterParams.s0p0}))")); + assertThat(filter, is("(request_path = #{filterParams.s0p0})")); assertThat(filterParameters.size(), is(1)); assertThat(filterParameters.get("s0p0"), is("/baz/")); } diff --git a/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Features.js b/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Features.js index a36b2a929d..d5c95e5deb 100644 --- a/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Features.js +++ b/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Features.js @@ -143,7 +143,7 @@ Ext.define('NX.controller.Features', { path = clonedFeature.mode + '/' + path; clonedFeature.path = '/' + path; - model = me.getStore('Feature').getById(clonedFeature.path); + model = me.getStore('Feature').getById(clonedFeature.id); if (model) { me.getStore('Feature').remove(model); } diff --git a/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Menu.js b/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Menu.js index bf57a11c3f..c2ddb3261d 100644 --- a/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Menu.js +++ b/components/nexus-rapture/src/main/resources/static/rapture/NX/controller/Menu.js @@ -245,7 +245,7 @@ Ext.define('NX.controller.Menu', { if (me.bookmarkingEnabled) { me.bookmark(featureMenuModel); } - me.selectFeature(me.getStore('Feature').getById(featureMenuModel.get('path'))); + me.selectFeature(me.getStore('Feature').getById(featureMenuModel.get('id'))); me.populateFeatureGroupStore(featureMenuModel); } }, @@ -287,7 +287,7 @@ Ext.define('NX.controller.Menu', { // add all children of the record to the group store, but do not include the node for the current record record.eachChild(function (node) { node.cascadeBy(function (child) { - features.push(featureStore.getById(child.get('path'))); + features.push(featureStore.getById(child.get('id'))); }); }); @@ -571,6 +571,7 @@ Ext.define('NX.controller.Menu', { expanded: nodeExpandMap[feature.path] === undefined ? feature.expanded : nodeExpandMap[feature.path], helpKeyword: feature.helpKeyword, iconName: feature.iconName, + id: feature.id, mode: feature.mode, path: feature.path, text: feature.text, @@ -629,6 +630,7 @@ Ext.define('NX.controller.Menu', { */ createNotAvailableFeature: function (feature) { return this.getFeatureModel().create({ + id: feature.get('id'), text: feature.get('text'), path: feature.get('path'), description: feature.get('description'), diff --git a/components/nexus-rapture/src/main/resources/static/rapture/NX/model/Feature.js b/components/nexus-rapture/src/main/resources/static/rapture/NX/model/Feature.js index 647a7175d4..3ce57447bf 100644 --- a/components/nexus-rapture/src/main/resources/static/rapture/NX/model/Feature.js +++ b/components/nexus-rapture/src/main/resources/static/rapture/NX/model/Feature.js @@ -24,11 +24,12 @@ Ext.define('NX.model.Feature', { extend: 'Ext.data.Model', - idProperty: 'path', + idProperty: 'id', // FIXME: define types so its clear what this data is! Also consider comments for further clarity. fields: [ + { name: 'id' }, { name: 'path' }, { name: 'text' }, { diff --git a/components/nexus-rapture/src/main/resources/static/rapture/NX/model/FeatureMenu.js b/components/nexus-rapture/src/main/resources/static/rapture/NX/model/FeatureMenu.js index 4aaa075816..e5330d3217 100644 --- a/components/nexus-rapture/src/main/resources/static/rapture/NX/model/FeatureMenu.js +++ b/components/nexus-rapture/src/main/resources/static/rapture/NX/model/FeatureMenu.js @@ -26,9 +26,8 @@ Ext.define('NX.model.FeatureMenu', { // FIXME: define types so its clear what this data is! Also consider comments for further clarity. - // FIXME: Set ID for module... unsure what this should be in a tree though - fields: [ + { name: 'id' }, { name: 'path' }, { /** diff --git a/components/nexus-repository-config/src/main/java/org/sonatype/nexus/repository/routing/RoutingRuleHelper.java b/components/nexus-repository-config/src/main/java/org/sonatype/nexus/repository/routing/RoutingRuleHelper.java index 79b893705f..7339d10a53 100644 --- a/components/nexus-repository-config/src/main/java/org/sonatype/nexus/repository/routing/RoutingRuleHelper.java +++ b/components/nexus-repository-config/src/main/java/org/sonatype/nexus/repository/routing/RoutingRuleHelper.java @@ -62,4 +62,12 @@ public interface RoutingRuleHelper * Ensures that the user has the necessary permissions to Read routing rules */ void ensureUserHasPermissionToRead(); + + /** + * Determine if the repository has a routing rule configured. + * + * @param repository the repository for the context of this request + * @return true if the repository has a routing rule configured + */ + boolean hasRoutingRule(final Repository repository); } diff --git a/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/manager/internal/RepositoryManagerImpl.java b/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/manager/internal/RepositoryManagerImpl.java index ae2ddbd9c6..1e62f76bdd 100644 --- a/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/manager/internal/RepositoryManagerImpl.java +++ b/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/manager/internal/RepositoryManagerImpl.java @@ -219,8 +219,14 @@ private void track(final Repository repository) { // configure security securityContributor.add(repository); - log.debug("Tracking: {}", repository); - repositories.put(repository.getName().toLowerCase(), repository); + Repository value = repositories.putIfAbsent(repository.getName().toLowerCase(), repository); + + if (value == null) { + log.debug("Tracking: {}", repository); + } + else { + log.debug("An existing repository with the same name is already tracked {}", value); + } } /** diff --git a/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/routing/internal/RoutingRuleHelperImpl.java b/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/routing/internal/RoutingRuleHelperImpl.java index d5aa0fe69d..148e4becc7 100644 --- a/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/routing/internal/RoutingRuleHelperImpl.java +++ b/components/nexus-repository-services/src/main/java/org/sonatype/nexus/repository/routing/internal/RoutingRuleHelperImpl.java @@ -94,6 +94,13 @@ public Map> calculateAssignedRepositories() { .collect(groupingBy(routingRuleCache::getRoutingRuleId, toList())); } + @Override + public boolean hasRoutingRule(final Repository repository) { + RoutingRule routingRule = routingRuleCache.getRoutingRule(repository); + + return routingRule != null; + } + @Override public void ensureUserHasPermissionToRead() { List permissions = getRepositoryAddPermissions(); diff --git a/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.jsx b/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.jsx new file mode 100644 index 0000000000..e8d38c15d8 --- /dev/null +++ b/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.jsx @@ -0,0 +1,33 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; +import {NxReadOnly} from '@sonatype/react-shared-components'; +import {isNil, isEmpty} from 'ramda'; + +/** + * @param {string} label + * @param value [required] + * @return {ReactNode} + */ +export default function ReadOnlyField({label, value}) { + const valid = !isNil(value) && !isEmpty(value); + + return ( + valid && ( + <> + {label} + {value} + + ) + ); +} diff --git a/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.test.jsx b/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.test.jsx new file mode 100644 index 0000000000..e4f13ec99d --- /dev/null +++ b/components/nexus-ui-plugin/src/frontend/src/components/widgets/ReadOnlyField/ReadOnlyField.test.jsx @@ -0,0 +1,61 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import ReadOnlyField from './ReadOnlyField'; + +describe('readOnlyRenderField', () => { + const label = 'Best Label'; + const data = 'email@test.com'; + + const selectors = { + label: () => screen.getByText(label), + data: () => screen.getByText(data), + queryLabel: () => screen.queryByText(label), + queryData: () => screen.queryByText(data), + }; + + const renderComponent = () => + render(); + + it('returns an element', () => { + renderComponent(); + + expect(selectors.label()).toBeInTheDocument(); + expect(selectors.data()).toBeInTheDocument(); + }); + + it('does not return an element if the data is empty, null or undefined', async () => { + const {rerender} = renderComponent(); + + rerender(); + + expect(selectors.queryLabel()).not.toBeInTheDocument(); + expect(selectors.queryData()).not.toBeInTheDocument(); + + rerender(); + + expect(selectors.queryLabel()).not.toBeInTheDocument(); + expect(selectors.queryData()).not.toBeInTheDocument(); + + rerender(); + + expect(selectors.queryLabel()).not.toBeInTheDocument(); + expect(selectors.queryData()).not.toBeInTheDocument(); + + rerender(); + + expect(selectors.queryLabel()).not.toBeInTheDocument(); + expect(selectors.queryData()).not.toBeInTheDocument(); + }); +}); diff --git a/components/nexus-ui-plugin/src/frontend/src/index.js b/components/nexus-ui-plugin/src/frontend/src/index.js index 1f3058d523..e0a0d1aad6 100644 --- a/components/nexus-ui-plugin/src/frontend/src/index.js +++ b/components/nexus-ui-plugin/src/frontend/src/index.js @@ -54,6 +54,7 @@ export { default as FieldWrapper } from './components/widgets/FieldWrapper/Field export { default as HelpTile } from './components/widgets/HelpTile/HelpTile'; export { default as Information } from './components/widgets/Information/Information'; export { default as Select } from './components/widgets/Select/Select'; +export { default as ReadOnlyField } from './components/widgets/ReadOnlyField/ReadOnlyField'; export { default as SslCertificateDetailsModal } from './components/widgets/SslCertificateDetailsModal/SslCertificateDetailsModal'; diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.jsx index 072cb0fe18..559702e3a8 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.jsx @@ -11,7 +11,7 @@ * Eclipse Foundation. All other trademarks are the property of their respective owners. */ import React from 'react'; - +import {useMachine} from '@xstate/react'; import { NxTile, } from '@sonatype/react-shared-components'; @@ -27,11 +27,15 @@ import {faEnvelope} from '@fortawesome/free-solid-svg-icons'; import EmailServerForm from './EmailServerForm'; import EmailServerReadOnly from './EmailServerReadOnly'; +import EmailVerifyServer from './EmailVerifyServer'; import UIStrings from '../../../../constants/UIStrings'; +import Machine from './EmailServerMachine'; + export default function EmailServer() { const canEdit = ExtJS.checkPermission('nexus:settings:update'); + const stateMachine = useMachine(Machine, {devTools: true}); return @@ -40,10 +44,15 @@ export default function EmailServer() { {canEdit - ? + ? : } + {canEdit && + + + + } ; } diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.test.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.test.jsx index 3fa5d10342..585027b956 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.test.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServer.test.jsx @@ -11,7 +11,13 @@ * Eclipse Foundation. All other trademarks are the property of their respective owners. */ import React from 'react'; -import {render, screen, waitForElementToBeRemoved, waitFor} from '@testing-library/react'; +import { + render, + screen, + waitForElementToBeRemoved, + waitFor, + act +} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import {ExtJS, TestUtils, APIConstants} from '@sonatype/nexus-ui-plugin'; import {when} from 'jest-when'; @@ -20,7 +26,15 @@ import Axios from 'axios'; import EmailServer from './EmailServer'; import UIStrings from '../../../../constants/UIStrings'; -const {EMAIL_SERVER: {FORM: LABELS}, SETTINGS, USE_TRUST_STORE, ERROR} = UIStrings; +const { + EMAIL_SERVER: { + FORM: LABELS, + VERIFY, + READ_ONLY + }, + SETTINGS, + ERROR +} = UIStrings; const {REST: {PUBLIC: {EMAIL_SERVER: emailServerUrl}}} = APIConstants; const XSS_STRING = TestUtils.XSS_STRING; @@ -55,6 +69,45 @@ const selectors = { identityCheck: () => screen.queryByLabelText(LABELS.SSL_TLS_OPTIONS.OPTIONS.IDENTITY_CHECK), discardButton: () => screen.getByText(SETTINGS.DISCARD_BUTTON_LABEL), saveButton: () => screen.getByText(SETTINGS.SAVE_BUTTON_LABEL), + test: { + input: () => screen.getByLabelText(VERIFY.LABEL), + button: () => screen.getByText(VERIFY.TEST), + success: () => screen.getByText(VERIFY.SUCCESS), + querySuccess: () => screen.queryByText(VERIFY.SUCCESS), + failed: () => screen.getByText(VERIFY.ERROR), + queryFailed: () => screen.queryByText(VERIFY.ERROR), + }, + readOnly: { + title: () => screen.getByText(LABELS.SECTIONS.SETUP), + enabled: () => screen.getByText(LABELS.ENABLED.LABEL), + enabledValue: () => screen.getByText(LABELS.ENABLED.LABEL).nextSibling, + warning: () => screen.getByText(UIStrings.SETTINGS.READ_ONLY.WARNING), + host: () => screen.getByText(LABELS.HOST.LABEL), + hostValue: () => screen.getByText(LABELS.HOST.LABEL).nextSibling, + port: () => screen.getByText(LABELS.PORT.LABEL), + portValue: () => screen.getByText(LABELS.PORT.LABEL).nextSibling, + username: () => screen.getByText(LABELS.USERNAME.LABEL), + usernameValue: () => screen.getByText(LABELS.USERNAME.LABEL).nextSibling, + fromAddress: () => screen.getByText(LABELS.FROM_ADDRESS.LABEL), + fromAddressValue: () => screen.getByText(LABELS.FROM_ADDRESS.LABEL).nextSibling, + subjectPrefix: () => screen.getByText(LABELS.SUBJECT_PREFIX.LABEL), + subjectPrefixValue: () => screen.getByText(LABELS.SUBJECT_PREFIX.LABEL).nextSibling, + options: () => screen.getByText(LABELS.SSL_TLS_OPTIONS.LABEL), + optionsValues: { + enable: [ + () => screen.getByText(READ_ONLY.ENABLE.ENABLE_STARTTLS), + () => screen.getByText(READ_ONLY.ENABLE.REQUIRE_STARTTLS), + () => screen.getByText(READ_ONLY.ENABLE.ENABLE_SSL_TLS), + () => screen.getByText(READ_ONLY.ENABLE.IDENTITY_CHECK) + ], + notEnable: [ + () => screen.getByText(READ_ONLY.NOT_ENABLE.ENABLE_STARTTLS), + () => screen.getByText(READ_ONLY.NOT_ENABLE.REQUIRE_STARTTLS), + () => screen.getByText(READ_ONLY.NOT_ENABLE.ENABLE_SSL_TLS), + () => screen.getByText(READ_ONLY.NOT_ENABLE.IDENTITY_CHECK) + ] + } + } }; const DATA = { @@ -121,7 +174,6 @@ const formShouldBeEmpty = () => { }; describe('EmailServer', () => { - const renderAndWaitForLoad = async () => { render(); await waitForElementToBeRemoved(selectors.queryLoadingMask()); @@ -272,4 +324,195 @@ describe('EmailServer', () => { userEvent.click(discardButton()); formShouldBeEmpty(); }); + + describe('Verify configuration', () => { + const url = `${emailServerUrl}/verify`; + const email = 'example@test.com'; + + it('renders the empty input field', async () => { + const {input, button} = selectors.test; + + await renderAndWaitForLoad(); + + expect(input()).toBeInTheDocument(); + expect(button()).toBeInTheDocument(); + }); + + it('validates the email configuration', async () => { + const {input, button, success} = selectors.test; + + when(Axios.post).calledWith(url, email).mockResolvedValue({data: {success: true}}); + + await renderAndWaitForLoad(); + + await TestUtils.changeField(input, email); + + await act(async () => userEvent.click(button())); + + expect(Axios.post).toHaveBeenCalledWith(url, email); + expect(success()).toBeInTheDocument(); + }); + + it('show error message if the validation fails', async () => { + const {input, button, failed} = selectors.test; + + when(Axios.post).calledWith(url, email).mockRejectedValue({data: {success: false}}); + + await renderAndWaitForLoad(); + + await TestUtils.changeField(input, email); + + await act(async () => userEvent.click(button())); + + expect(Axios.post).toHaveBeenCalledWith(url, email); + expect(failed()).toBeInTheDocument(); + }); + + it('validate email', async () => { + const {input, button} = selectors.test; + + await renderAndWaitForLoad(); + + expect(button()).toBeDisabled(); + + await TestUtils.changeField(input, 'wrong_email'); + + expect(button()).toBeDisabled(); + }); + + it('removes success message if the input value changes', async () => { + const {input, button, success, querySuccess} = selectors.test; + + when(Axios.post).calledWith(url, email).mockResolvedValue({data: {success: true}}); + + await renderAndWaitForLoad(); + + await TestUtils.changeField(input, email); + + await act(async () => userEvent.click(button())); + + expect(Axios.post).toHaveBeenCalledWith(url, email); + expect(success()).toBeInTheDocument(); + + await TestUtils.changeField(input, 'changes'); + + expect(querySuccess()).not.toBeInTheDocument(); + }); + + it('removes error message if the input value changes', async () => { + const {input, button, failed, queryFailed} = selectors.test; + + when(Axios.post).calledWith(url, email).mockRejectedValue({data: {success: false}}); + + await renderAndWaitForLoad(); + + await TestUtils.changeField(input, email); + + await act(async () => userEvent.click(button())); + + expect(Axios.post).toHaveBeenCalledWith(url, email); + expect(failed()).toBeInTheDocument(); + + await TestUtils.changeField(input, 'changes'); + + expect(queryFailed()).not.toBeInTheDocument(); + }); + }); + + describe('Read only', () => { + const data = { + enabled: true, + host: "smtp.gmail.com", + port: 465, + username: "my_user@sonatype.com", + password: null, + fromAddress: "test@sonatype.com", + subjectPrefix: "subject", + startTlsEnabled: true, + startTlsRequired: true, + sslOnConnectEnabled: true, + sslServerIdentityCheckEnabled: true, + nexusTrustStoreEnabled: true + } + + beforeEach(() => { + when(ExtJS.checkPermission) + .calledWith('nexus:settings:update') + .mockReturnValue(false); + + when(Axios.get) + .calledWith(emailServerUrl) + .mockResolvedValue({data}); + }); + + it('shows default information if email server is not enabled', async () => { + when(Axios.get).calledWith(emailServerUrl).mockResolvedValue({data: {}}); + + const {title, enabled, warning, enabledValue} = selectors.readOnly; + + await renderAndWaitForLoad(); + + expect(title()).toBeInTheDocument(); + expect(warning()).toBeInTheDocument(); + expect(enabled()).toBeInTheDocument(); + expect(enabledValue()).toHaveTextContent('Disabled'); + }); + + it('shows the configuration correctly', async () => { + const { + title, + warning, + enabled, + enabledValue, + host, + hostValue, + port, + portValue, + username, + usernameValue, + fromAddress, + fromAddressValue, + subjectPrefix, + subjectPrefixValue, + options, + optionsValues + } = selectors.readOnly; + + await renderAndWaitForLoad(); + + expect(title()).toBeInTheDocument(); + expect(warning()).toBeInTheDocument(); + expect(enabled()).toBeInTheDocument(); + expect(enabledValue()).toHaveTextContent('Enabled'); + expect(host()).toBeInTheDocument(); + expect(hostValue()).toHaveTextContent(data.host); + expect(port()).toBeInTheDocument(); + expect(portValue()).toHaveTextContent(data.port); + expect(username()).toBeInTheDocument(); + expect(usernameValue()).toHaveTextContent(data.username); + expect(fromAddress()).toBeInTheDocument(); + expect(fromAddressValue()).toHaveTextContent(data.fromAddress); + expect(subjectPrefix()).toBeInTheDocument(); + expect(subjectPrefixValue()).toHaveTextContent(data.subjectPrefix); + expect(options()).toBeInTheDocument(); + optionsValues.enable.forEach((value) => expect(value()).toBeInTheDocument()); + }); + + it('shows the corresponding message when SSL/TLS options are not enabled', async () => { + const {optionsValues} = selectors.readOnly; + const newData = { + ...data, + startTlsEnabled: false, + startTlsRequired: false, + sslOnConnectEnabled: false, + sslServerIdentityCheckEnabled: false, + } + + when(Axios.get).calledWith(emailServerUrl).mockResolvedValue({data:newData}); + + await renderAndWaitForLoad(); + + optionsValues.notEnable.forEach((value) => expect(value()).toBeInTheDocument()); + }); + }) }); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerForm.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerForm.jsx index ab584cd06e..97e1d2c356 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerForm.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerForm.jsx @@ -11,13 +11,7 @@ * Eclipse Foundation. All other trademarks are the property of their respective owners. */ import React from 'react'; -import {useMachine} from '@xstate/react'; - -import { - FormUtils, - ValidationUtils, - UseNexusTruststore, -} from '@sonatype/nexus-ui-plugin'; +import {FormUtils, ValidationUtils} from '@sonatype/nexus-ui-plugin'; import { NxForm, NxFormGroup, @@ -33,16 +27,12 @@ import { import UIStrings from '../../../../constants/UIStrings'; -import Machine from './EmailServerMachine'; - const {EMAIL_SERVER: {FORM: LABELS}} = UIStrings; -export default function EmailServerForm() { - const [current, send] = useMachine(Machine, {devTools: true}); - +export default function EmailServerForm({ parentMachine }) { + const [current, send] = parentMachine; const { isPristine, - data: {host, port}, loadError, saveError, validationErrors, diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerReadOnly.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerReadOnly.jsx index f220333c00..2dbc3552e2 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerReadOnly.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailServerReadOnly.jsx @@ -11,7 +11,65 @@ * Eclipse Foundation. All other trademarks are the property of their respective owners. */ import React from 'react'; +import {useMachine} from '@xstate/react'; +import { + NxReadOnly, + NxLoadWrapper, + NxInfoAlert, + NxH2, +} from '@sonatype/react-shared-components'; + +import {FormUtils, ReadOnlyField} from '@sonatype/nexus-ui-plugin'; + +import Machine from './EmailServerMachine'; + +import UIStrings from '../../../../constants/UIStrings'; + +const {FORM: LABELS, READ_ONLY} = UIStrings.EMAIL_SERVER; +const {readOnlyCheckboxValueLabel} = FormUtils; export default function EmailServerReadOnly() { - return 'Email Server Read-Only Mode'; + const [current, send] = useMachine(Machine, {devTools: true}); + const {data, loadError} = current.context; + const isLoading = current.matches('loading'); + const enabled = readOnlyCheckboxValueLabel(data.enabled); + const retry = () => send('RETRY'); + + const renderData = (label, value) => { + const enable = value ? 'ENABLE' : 'NOT_ENABLE'; + return {READ_ONLY[enable][label]}; + }; + + return ( + + {LABELS.SECTIONS.SETUP} + {UIStrings.SETTINGS.READ_ONLY.WARNING} + + + {data.enabled && ( + <> + + + + + + {LABELS.SSL_TLS_OPTIONS.LABEL} + {renderData('ENABLE_STARTTLS', data.startTlsEnabled)} + {renderData('REQUIRE_STARTTLS', data.startTlsRequired)} + {renderData('ENABLE_SSL_TLS', data.sslOnConnectEnabled)} + {renderData('IDENTITY_CHECK', data.sslServerIdentityCheckEnabled)} + + )} + + + ); } diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServer.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServer.jsx new file mode 100644 index 0000000000..399eeaa1fb --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServer.jsx @@ -0,0 +1,102 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; +import {useMachine} from '@xstate/react'; +import {isEmpty} from 'ramda'; +import { + NxFormGroup, + NxTextInput, + NxFormRow, + NxButton, + NxSuccessAlert, + NxErrorAlert, + NxLoadWrapper, +} from '@sonatype/react-shared-components'; +import {FormUtils, ValidationUtils} from '@sonatype/nexus-ui-plugin'; + +import UIStrings from '../../../../constants/UIStrings'; +const {EMAIL_SERVER: LABELS} = UIStrings; + +import Machine from './EmailVerifyServerMachine'; + +export default function EmailVerifyServer({parentMachine}) { + const [parentState] = parentMachine; + const {isPristine} = parentState.context; + + const initialState = { + email: '', + testResult: null, + }; + + const [current, send] = useMachine(Machine, { + context: { + data: initialState, + pristineData: initialState, + }, + devTools: true, + }); + + const {validationErrors, data, saveError, testResult, isTouched} = + current.context; + const isSaving = current.matches('saving'); + const isInvalid = FormUtils.isInvalid(validationErrors); + const disabled = + isInvalid || isSaving || ValidationUtils.isBlank(data.email) || !isPristine; + + const onTest = () => send('SAVE'); + + const handleEnter = (event) => { + if (event.key === 'Enter') { + onTest(); + } + }; + + const success = isEmpty(isTouched) && testResult === true; + const error = isEmpty(isTouched) && testResult === false; + + return ( + <> + + <> + + + + + {LABELS.VERIFY.TEST} + + + + + {success && {LABELS.VERIFY.SUCCESS}} + {error && {LABELS.VERIFY.ERROR}} + + + ); +} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServerMachine.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServerMachine.js new file mode 100644 index 0000000000..9b19783d09 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/EmailServer/EmailVerifyServerMachine.js @@ -0,0 +1,60 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Open Source Version is distributed with Sencha Ext JS pursuant to a FLOSS Exception agreed upon + * between Sonatype, Inc. and Sencha Inc. Sencha Ext JS is licensed under GPL v3 and cannot be redistributed as part of a + * closed source work. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import {assign} from 'xstate'; +import Axios from 'axios'; +import { + FormUtils, + APIConstants, + ValidationUtils, +} from '@sonatype/nexus-ui-plugin'; + +const { + REST: { + PUBLIC: {VERIFY_EMAIL_SERVER}, + }, +} = APIConstants; + +export default FormUtils.buildFormMachine({ + id: 'EmailVerifyServerMachine', + initial: 'loaded', +}).withConfig({ + actions: { + validate: assign({ + validationErrors: ({data}) => ({ + email: + ValidationUtils.notBlank(data.email) && + ValidationUtils.validateEmail(data.email), + }), + }), + logSaveSuccess: () => {}, + logLoadError: () => {}, + setSavedData: assign({ + isTouched: () => ({}), + testResult: (_, event) => { + const result = event.data.data; + return result.success; + }, + }), + setSaveError: assign({ + isTouched: () => ({}), + testResult: () => false, + }), + }, + services: { + saveData: ({data: {email}}) => Axios.post(VERIFY_EMAIL_SERVER, email), + }, +}); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Http/HttpReadOnly.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Http/HttpReadOnly.jsx index f47fa4ae87..0ff9eef8fd 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Http/HttpReadOnly.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Http/HttpReadOnly.jsx @@ -12,7 +12,6 @@ */ import React from 'react'; import {useMachine} from '@xstate/react'; -import {isNil} from 'ramda'; import { NxReadOnly, NxLoadWrapper, @@ -22,8 +21,11 @@ import { NxH2, } from '@sonatype/react-shared-components'; +import {FormUtils, ReadOnlyField} from '@sonatype/nexus-ui-plugin'; + import UIStrings from '../../../../constants/UIStrings'; +const {readOnlyRenderField} = FormUtils; const {CONFIGURATION: LABELS} = UIStrings.HTTP; import Machine from './HttpMachine'; @@ -38,29 +40,27 @@ export default function HttpReadOnly() { const retry = () => send('RETRY'); - const renderField = (label, data) => { - return ( - !isNil(data) && ( - <> - {label} - {data} - - ) - ); - }; - return ( {LABELS.READ_ONLY.LABEL} {LABELS.READ_ONLY.WARNING} - {renderField(LABELS.USER_AGENT.LABEL, data.userAgentSuffix)} - {renderField(LABELS.TIMEOUT.LABEL, data.timeout)} - {renderField(LABELS.ATTEMPTS.LABEL, data.retries)} + + + {httpEnabled && ( <> - {renderField(LABELS.PROXY.HTTP_HOST.LABEL, data.httpHost)} - {renderField(LABELS.PROXY.HTTP_PORT, data.httpPort)} + + )} {httpEnabled && httpAuthEnabled && ( @@ -71,16 +71,31 @@ export default function HttpReadOnly() { {LABELS.PROXY.HTTP_AUTHENTICATION} - {renderField(LABELS.PROXY.USERNAME, data.httpAuthUsername)} - {renderField(LABELS.PROXY.HOST_NAME, data.httpAuthNtlmHost)} - {renderField(LABELS.PROXY.DOMAIN, data.httpAuthNtlmDomain)} + + + )} {isHttpsEnabled && ( <> - {renderField(LABELS.PROXY.HTTPS_HOST, data.httpsHost)} - {renderField(LABELS.PROXY.HTTPS_PORT, data.httpsPort)} + + )} {isHttpsEnabled && httpsAuthEnabled && ( @@ -91,9 +106,18 @@ export default function HttpReadOnly() { {LABELS.PROXY.HTTPS_AUTHENTICATION} - {renderField(LABELS.PROXY.USERNAME, data.httpsAuthUsername)} - {renderField(LABELS.PROXY.HOST_NAME, data.httpsAuthNtlmHost)} - {renderField(LABELS.PROXY.DOMAIN, data.httpsAuthNtlmDomain)} + + + )} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Privileges/Privileges.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Privileges/Privileges.jsx index c146854f2e..ab93040ab3 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Privileges/Privileges.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Privileges/Privileges.jsx @@ -18,7 +18,7 @@ import PrivilegesList from './PrivilegesList'; import PrivilegesDetails from './PrivilegesDetails'; export default function Privileges() { - return + return diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/Repositories.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/Repositories.jsx index f37aa9edb2..19fcc0d018 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/Repositories.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/Repositories.jsx @@ -21,7 +21,7 @@ import RepositoriesContextProvider from './RepositoriesContextProvider'; export default function Repositories() { return ( - + diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoriesForm.test.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoriesForm.test.jsx index 649d4ee2a7..42fd6fe55c 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoriesForm.test.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoriesForm.test.jsx @@ -53,7 +53,8 @@ jest.mock('@sonatype/nexus-ui-plugin', () => ({ checkPermission: jest.fn(), requestConfirmation: jest.fn(), state: () => ({ - getValue: jest.fn(() => false) + getValue: jest.fn(() => false), + getEdition: jest.fn(() => 'PRO') }) } })); @@ -79,7 +80,13 @@ jest.mock('./RepositoriesContextProvider', () => ({ })); const { - REPOSITORIES: {EDITOR}, + REPOSITORIES: { + EDITOR, + EDITOR: { + APT, + DOCKER: {INDEX, CONNECTORS} + } + }, SETTINGS, USE_TRUST_STORE, CLOSE @@ -168,18 +175,22 @@ describe('RepositoriesForm', () => { getRemoveQuarantinedCheckbox: () => screen.getByRole('checkbox', {name: EDITOR.NPM.REMOVE_QUARANTINED.DESCR}), getVersionPolicySelect: () => screen.getByLabelText(EDITOR.VERSION_POLICY_LABEL), - getDockerConnectorHttpPortCheckbox: () => + getDockerSubdomainCheckbox: () => screen.getAllByRole('checkbox', {name: 'Toggle Text Input'})[0], - getDockerConnectorHttpsPortCheckbox: () => + getDockerConnectorHttpPortCheckbox: () => screen.getAllByRole('checkbox', {name: 'Toggle Text Input'})[1], - getDockerConnectorHttpPort: () => - screen.getAllByPlaceholderText(EDITOR.DOCKER_CONNECTOR_PLACEHOLDER)[0], - getDockerConnectorHttpsPort: () => - screen.getAllByPlaceholderText(EDITOR.DOCKER_CONNECTOR_PLACEHOLDER)[1], + getDockerConnectorHttpsPortCheckbox: () => + screen.getAllByRole('checkbox', {name: 'Toggle Text Input'})[2], + getDockerSubdomainInput: () => + screen.getByPlaceholderText(CONNECTORS.SUBDOMAIN.PLACEHOLDER), + getDockerConnectorHttpPortInput: () => + screen.getAllByPlaceholderText(CONNECTORS.HTTP.PLACEHOLDER)[0], + getDockerConnectorHttpsPortInput: () => + screen.getAllByPlaceholderText(CONNECTORS.HTTPS.PLACEHOLDER)[1], getDockerApiVersionCheckbox: () => screen.getByRole('checkbox', {name: EDITOR.REGISTRY_API_SUPPORT_DESCR}), getDockerAnonimousPullCheckbox: () => - screen.getByRole('checkbox', {name: EDITOR.ALLOW_ANON_DOCKER_PULL_DESCR}), + screen.getByRole('checkbox', {name: CONNECTORS.ALLOW_ANON_DOCKER_PULL.DESCR}), getDockerWritableRepositorySelect: () => screen.getByLabelText(EDITOR.WRITABLE.LABEL), getDockerRedeployLatestCheckboxEnabled: () => screen.getByRole('checkbox', {name: EDITOR.REDEPLOY_LATEST.DESCRIPTION}), @@ -191,9 +202,9 @@ describe('RepositoriesForm', () => { screen.getByRole('button', {name: USE_TRUST_STORE.VIEW_CERTIFICATE}), getCloseCertificateButton: () => screen.getByText(CLOSE, {selector: 'button'}), getDockerIndexRadioButtons: () => ({ - registry: screen.getByLabelText(EDITOR.DOCKER.INDEX.OPTIONS.REGISTRY), - hub: screen.getByLabelText(EDITOR.DOCKER.INDEX.OPTIONS.HUB), - custom: screen.getByLabelText(EDITOR.DOCKER.INDEX.OPTIONS.CUSTOM) + registry: screen.getByLabelText(INDEX.OPTIONS.REGISTRY), + hub: screen.getByLabelText(INDEX.OPTIONS.HUB), + custom: screen.getByLabelText(INDEX.OPTIONS.CUSTOM) }), getDockerIndexUrlInput: () => screen.queryByLabelText(EDITOR.DOCKER.INDEX.URL.LABEL), getPreemptiveAuthCheckbox: () => { @@ -206,7 +217,11 @@ describe('RepositoriesForm', () => { getForeignLayerRemoveButton: (item) => { const listItem = screen.getByText(item).closest('.nx-list__item'); return item && within(listItem).getByTitle(EDITOR.FOREIGN_LAYER.REMOVE); - } + }, + getAptDistributionInput: () => screen.getByLabelText(APT.DISTRIBUTION.LABEL), + getAptFlatCheckbox: () => screen.getByRole('checkbox', {name: APT.FLAT.DESCR}), + getAptKeyInput: () => screen.getByLabelText(APT.SIGNING.KEY.LABEL), + getAptPassphraseInput: () => screen.getByLabelText(APT.SIGNING.PASSPHRASE.LABEL) }; const renderView = (itemId = '') => { @@ -228,6 +243,8 @@ describe('RepositoriesForm', () => { const onDone = jest.fn(); const RECIPES_RESPONSE = [ + {format: 'apt', type: 'hosted'}, + {format: 'apt', type: 'proxy'}, {format: 'bower', type: 'proxy'}, {format: 'docker', type: 'proxy'}, {format: 'docker', type: 'hosted'}, @@ -487,6 +504,47 @@ describe('RepositoriesForm', () => { userEvent.click(selectors.getDockerRedeployLatestCheckboxEnabled()); + userEvent.click(selectors.getDockerSubdomainCheckbox()); + await TestUtils.changeField(selectors.getDockerSubdomainInput, 'docker-sub-domain'); + userEvent.click(selectors.getDockerSubdomainCheckbox()); + + userEvent.click(selectors.getCreateButton()); + + await waitFor(() => expect(Axios.post).toHaveBeenCalledWith(getSaveUrl(repo), repo)); + }); + + it('creates apt hosted repository', async () => { + const repo = { + format: 'apt', + type: 'hosted', + name: 'apt-hosted-1', + online: true, + storage: { + blobStoreName: 'default', + strictContentTypeValidation: true, + writePolicy: 'ALLOW_ONCE' + }, + component: { + proprietaryComponents: false + }, + cleanup: null, + apt: { + distribution: 'bionic' + }, + aptSigning: { + keypair: 'apt-key-pair', + passphrase: 'apt-key-pass' + } + }; + + await renderViewAndSetRequiredFields(repo); + + await TestUtils.changeField(selectors.getBlobStoreSelect, repo.storage.blobStoreName); + + await TestUtils.changeField(selectors.getAptDistributionInput, repo.apt.distribution); + await TestUtils.changeField(selectors.getAptKeyInput, repo.aptSigning.keypair); + await TestUtils.changeField(selectors.getAptPassphraseInput, repo.aptSigning.passphrase); + userEvent.click(selectors.getCreateButton()); await waitFor(() => expect(Axios.post).toHaveBeenCalledWith(getSaveUrl(repo), repo)); @@ -991,7 +1049,7 @@ describe('RepositoriesForm', () => { httpsPort: null, forceBasicAuth: false, v1Enabled: false, - subdomain: null + subdomain: 'docker-proxy-2' }, dockerProxy: { indexType: 'CUSTOM', @@ -1025,6 +1083,57 @@ describe('RepositoriesForm', () => { expect(selectors.getDockerIndexUrlInput()).toHaveValue(DOCKER_HUB_URL); userEvent.click(indexRadioButtons.custom); await TestUtils.changeField(selectors.getDockerIndexUrlInput, repo.dockerProxy.indexUrl); + userEvent.click(selectors.getDockerSubdomainCheckbox()); + + userEvent.click(selectors.getCreateButton()); + + await waitFor(() => expect(Axios.post).toHaveBeenCalledWith(getSaveUrl(repo), repo)); + }); + + it('creates apt proxy repository', async () => { + const repo = { + format: 'apt', + type: 'proxy', + name: 'apt-proxy-1', + online: true, + routingRule: '', + storage: { + blobStoreName: 'default', + strictContentTypeValidation: true + }, + cleanup: null, + proxy: { + remoteUrl: 'https://foo.bar', + contentMaxAge: 1440, + metadataMaxAge: 1440 + }, + negativeCache: { + enabled: true, + timeToLive: 1440 + }, + httpClient: { + blocked: false, + autoBlock: true, + authentication: null, + connection: null + }, + replication: { + preemptivePullEnabled: false, + assetPathRegex: '' + }, + apt: { + distribution: 'bionic', + flat: true + } + }; + + await renderViewAndSetRequiredFields(repo); + + await TestUtils.changeField(selectors.getRemoteUrlInput, repo.proxy.remoteUrl); + await TestUtils.changeField(selectors.getBlobStoreSelect, repo.storage.blobStoreName); + + await TestUtils.changeField(selectors.getAptDistributionInput, repo.apt.distribution); + userEvent.click(selectors.getAptFlatCheckbox()); userEvent.click(selectors.getCreateButton()); @@ -1160,7 +1269,7 @@ describe('RepositoriesForm', () => { forceBasicAuth: true, httpPort: '333', httpsPort: '444', - subdomain: null + subdomain: 'docker-sub-domain' } }; @@ -1168,17 +1277,22 @@ describe('RepositoriesForm', () => { await TestUtils.changeField(selectors.getBlobStoreSelect, repo.storage.blobStoreName); - expect(selectors.getDockerConnectorHttpPort()).toBeDisabled(); - expect(selectors.getDockerConnectorHttpsPort()).toBeDisabled(); + expect(selectors.getDockerConnectorHttpPortInput()).toBeDisabled(); + expect(selectors.getDockerConnectorHttpsPortInput()).toBeDisabled(); + expect(selectors.getDockerSubdomainInput()).toBeDisabled(); userEvent.click(selectors.getDockerConnectorHttpPortCheckbox()); userEvent.click(selectors.getDockerConnectorHttpsPortCheckbox()); + userEvent.click(selectors.getDockerSubdomainCheckbox()); - expect(selectors.getDockerConnectorHttpPort()).toBeEnabled(); - expect(selectors.getDockerConnectorHttpsPort()).toBeEnabled(); + expect(selectors.getDockerConnectorHttpPortInput()).toBeEnabled(); + expect(selectors.getDockerConnectorHttpsPortInput()).toBeEnabled(); + expect(selectors.getDockerSubdomainInput()).toBeEnabled(); + expect(selectors.getDockerSubdomainInput()).toHaveValue(repo.name); - await TestUtils.changeField(selectors.getDockerConnectorHttpPort, repo.docker.httpPort); - await TestUtils.changeField(selectors.getDockerConnectorHttpsPort, repo.docker.httpsPort); + await TestUtils.changeField(selectors.getDockerConnectorHttpPortInput, repo.docker.httpPort); + await TestUtils.changeField(selectors.getDockerConnectorHttpsPortInput, repo.docker.httpsPort); + await TestUtils.changeField(selectors.getDockerSubdomainInput, repo.docker.subdomain); userEvent.click(selectors.getDockerApiVersionCheckbox()); userEvent.click(selectors.getDockerAnonimousPullCheckbox()); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormConfig.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormConfig.js index f96e84d3ae..03abc40871 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormConfig.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormConfig.js @@ -36,6 +36,9 @@ import NugetProxyConfiguration from './facets/NugetProxyConfiguration'; import NugetGroupConfiguration from './facets/NugetGroupConfiguration'; import WritableRepositoryConfiguration from './facets/WritableRepositoryConfiguration'; import PreEmptiveAuthConfiguration from './facets/PreEmptiveAuthConfiguration'; +import AptDistributionConfiguration from './facets/AptDistributionConfiguration'; +import AptSigningConfiguration from './facets/AptSigningConfiguration'; +import AptFlatConfiguration from './facets/AptFlatConfiguration'; import {genericDefaultValues} from './RepositoryFormDefaultValues'; import { @@ -43,11 +46,14 @@ import { validateDockerConnectorPort, validateDockerIndexUrl, validateNugetQueryCacheItemMaxAge, - validateWritableMember + validateWritableMember, + validateDockerSubdomain } from './RepositoryFormValidators'; import {mergeDeepRight} from 'ramda'; +import {ValidationUtils} from '@sonatype/nexus-ui-plugin'; + export const DOCKER_INDEX_TYPES = { registry: 'REGISTRY', hub: 'HUB', @@ -73,6 +79,44 @@ const genericFacets = { }; const repositoryFormats = { + apt_hosted: { + facets: [AptDistributionConfiguration, AptSigningConfiguration, ...genericFacets.hosted], + defaultValues: { + ...genericDefaultValues.hosted, + apt: { + distribution: null + }, + aptSigning: { + keypair: null, + passphrase: null + } + }, + validators: (data) => ({ + ...genericValidators.hosted(data), + apt: { + distribution: ValidationUtils.validateNotBlank(data.apt?.distribution) + }, + aptSigning: { + keypair: ValidationUtils.validateNotBlank(data.aptSigning?.keypair), + } + }) + }, + apt_proxy: { + facets: [AptDistributionConfiguration, AptFlatConfiguration, ...genericFacets.proxy], + defaultValues: { + ...genericDefaultValues.proxy, + apt: { + distribution: null, + flat: false + } + }, + validators: (data) => ({ + ...genericValidators.proxy(data), + apt: { + distribution: ValidationUtils.validateNotBlank(data.apt?.distribution) + } + }) + }, bower_proxy: { facets: [RewritePackageUrlsConfiguration, ...genericFacets.proxy], defaultValues: { @@ -243,7 +287,8 @@ const repositoryFormats = { ...genericValidators.proxy(data), docker: { httpPort: validateDockerConnectorPort(data, 'httpPort'), - httpsPort: validateDockerConnectorPort(data, 'httpsPort') + httpsPort: validateDockerConnectorPort(data, 'httpsPort'), + subdomain: validateDockerSubdomain(data) }, dockerProxy: { indexUrl: validateDockerIndexUrl(data) diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormValidators.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormValidators.js index b415ccb98d..fa367b66b8 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormValidators.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/RepositoryFormValidators.js @@ -108,6 +108,27 @@ export const validateDockerIndexUrl = (data) => { : ValidationUtils.validateNotBlank(indexUrl) || ValidationUtils.validateIsUrl(indexUrl); }; +export const validateDockerSubdomain = (data) => { + const {subdomain} = data.docker; + + if (subdomain === null) { + return null; + } + + const notBlank = ValidationUtils.validateNotBlank(subdomain); + + if (notBlank) { + return notBlank; + } + + const subdomainRegex = /^[A-Za-z][A-Za-z0-9\-]{0,61}[A-Za-z0-9]$/; + const isSubdomain = subdomainRegex.test(subdomain); + + if (!isSubdomain) { + return UIStrings.REPOSITORIES.EDITOR.DOCKER.CONNECTORS.SUBDOMAIN.VALIDATION_ERROR; + } +}; + export const validateNugetQueryCacheItemMaxAge = (data) => { const {queryCacheItemMaxAge} = data.nugetProxy; return ( diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptDistributionConfiguration.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptDistributionConfiguration.jsx new file mode 100644 index 0000000000..b40b6d3723 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptDistributionConfiguration.jsx @@ -0,0 +1,41 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; + +import {FormUtils} from '@sonatype/nexus-ui-plugin'; + +import {NxFormGroup, NxTextInput, NxH2} from '@sonatype/react-shared-components'; + +import UIStrings from '../../../../../constants/UIStrings'; + +const { + CAPTION, + DISTRIBUTION: {LABEL, SUBLABEL} +} = UIStrings.REPOSITORIES.EDITOR.APT; + +export default function AptDistributionConfiguration({parentMachine}) { + const [parentState, sendParent] = parentMachine; + + return ( + <> + {CAPTION} + + + + + ); +} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptFlatConfiguration.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptFlatConfiguration.jsx new file mode 100644 index 0000000000..1ea8f56885 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptFlatConfiguration.jsx @@ -0,0 +1,36 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; + +import {FormUtils} from '@sonatype/nexus-ui-plugin'; + +import {NxFieldset, NxCheckbox} from '@sonatype/react-shared-components'; + +import UIStrings from '../../../../../constants/UIStrings'; + +const {LABEL, DESCR} = UIStrings.REPOSITORIES.EDITOR.APT.FLAT; + +export default function AptFlatConfiguration({parentMachine}) { + const [parentState, sendParent] = parentMachine; + + return ( + + + {DESCR} + + + ); +} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptSigningConfiguration.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptSigningConfiguration.jsx new file mode 100644 index 0000000000..6754e8a9f9 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/AptSigningConfiguration.jsx @@ -0,0 +1,47 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; + +import {FormUtils} from '@sonatype/nexus-ui-plugin'; + +import {NxFormGroup, NxTextInput} from '@sonatype/react-shared-components'; + +import UIStrings from '../../../../../constants/UIStrings'; + +const {KEY, PASSPHRASE} = UIStrings.REPOSITORIES.EDITOR.APT.SIGNING; + +export default function AptSigningConfiguration({parentMachine}) { + const [parentState, sendParent] = parentMachine; + + return ( + <> + + + + + + + + + ); +} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/RepositoryConnectorsConfiguration.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/RepositoryConnectorsConfiguration.jsx index 9cf051504f..56dfb742ac 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/RepositoryConnectorsConfiguration.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/RepositoryConnectorsConfiguration.jsx @@ -13,49 +13,65 @@ import React from 'react'; import {NxH2, NxCheckbox, NxFieldset} from '@sonatype/react-shared-components'; -import {FormUtils} from '@sonatype/nexus-ui-plugin'; +import {FormUtils, ExtJS} from '@sonatype/nexus-ui-plugin'; import UIStrings from '../../../../../constants/UIStrings'; import ToggleableTextInput from './ToggleableTextInput/ToggleableTextInput'; -const {EDITOR} = UIStrings.REPOSITORIES; +const {CONNECTORS} = UIStrings.REPOSITORIES.EDITOR.DOCKER; export default function RepositoryConnectorsConfiguration({parentMachine}) { - const [currentParent, sendParent] = parentMachine; + const [parentState, sendParent] = parentMachine; + + const repositoryName = parentState.context.data.name; + + const isProEdition = ExtJS.state().getEdition() === 'PRO'; return ( <> - {EDITOR.REPOSITORY_CONNECTORS_CAPTION} + {CONNECTORS.CAPTION} + +

{CONNECTORS.HELP}

-

{EDITOR.DOCKER_CONNECTORS_HELP}

+ {isProEdition && ( + + )} - {EDITOR.ALLOW_ANON_DOCKER_PULL_DESCR} + {CONNECTORS.ALLOW_ANON_DOCKER_PULL.DESCR} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/ToggleableTextInput/ToggleableTextInput.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/ToggleableTextInput/ToggleableTextInput.jsx index bff2fa4b13..1e0054b193 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/ToggleableTextInput/ToggleableTextInput.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Repositories/facets/ToggleableTextInput/ToggleableTextInput.jsx @@ -23,6 +23,7 @@ import './ToggleableTextInput.scss'; export default function ToggleableTextInput({ parentMachine, contextPropName, + defaultValue = '', label, sublabel = '', placeholder = '', @@ -33,14 +34,25 @@ export default function ToggleableTextInput({ const actualValue = path(contextPropName.split('.'), currentParent.context.data); const [isEnabled, setIsEnabled] = useState(!!actualValue); + const [displayedValue, setDisplayedValue] = useState(actualValue ? actualValue.toString() : ''); const updateActualValue = (value) => sendParent({type: 'UPDATE', name: contextPropName, value}); const toggleCheckbox = () => { const newIsEnabled = !isEnabled; - const value = newIsEnabled ? displayedValue : null; - updateActualValue(value); + + if (newIsEnabled) { + if (displayedValue) { + updateActualValue(displayedValue); + } else { + updateActualValue(defaultValue); + setDisplayedValue(defaultValue); + } + } else { + updateActualValue(null) + } + setIsEnabled(newIsEnabled); }; diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/SslCertificates/SslCertificates.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/SslCertificates/SslCertificates.jsx index 3938a5d5ce..f4e7485196 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/SslCertificates/SslCertificates.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/SslCertificates/SslCertificates.jsx @@ -18,7 +18,7 @@ import SslCertificatesList from './SslCertificatesList'; import SslCertificatesDetails from './SslCertificatesDetails'; export default function SslCertificates() { - return + return diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/ConfirmAdminPasswordForm.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/ConfirmAdminPasswordForm.jsx index 9215ab217b..23b4d80966 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/ConfirmAdminPasswordForm.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/ConfirmAdminPasswordForm.jsx @@ -27,7 +27,12 @@ import UIStrings from '../../../../constants/UIStrings'; const {MODAL} = UIStrings.USERS; -export default function ConfirmAdminPasswordForm({actor}) { +export default function ConfirmAdminPasswordForm({ + actor, + title, + text, + confirmLabel, +}) { const [state, send] = useActor(actor); const {isPristine, loadError, saveError, validationErrors} = state.context; const isLoading = state.matches('loading'); @@ -48,19 +53,19 @@ export default function ConfirmAdminPasswordForm({actor}) { - {MODAL.CHANGE_PASSWORD} + {title} - {MODAL.TEXT} + {text} + return diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.jsx index 2e18e6ed97..0e85d0bd8a 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.jsx @@ -28,6 +28,7 @@ import { import Machine from './UsersFormMachine'; import UsersForm from './UsersForm'; import UsersReadOnly from './UsersReadOnly'; +import UsersToken from './UsersToken'; import UIStrings from '../../../../constants/UIStrings'; import {parseIdParameter, fullName} from "./UsersHelper"; @@ -60,6 +61,8 @@ export default function UsersDetails({itemId, onDone}) { const canEdit = hasEditPermission && !readOnly; const isEdit = ValidationUtils.notBlank(id); const showReadOnly = isEdit && !canEdit; + const edition = ExtJS.state().getEdition(); + const isPro = edition === 'PRO'; return @@ -75,6 +78,11 @@ export default function UsersDetails({itemId, onDone}) { : } + {isPro && isEdit && + + + + } ; } diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.test.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.test.jsx index 1afe11e4a1..595bd55b74 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.test.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersDetails.test.jsx @@ -32,9 +32,23 @@ import {ROLES} from './Users.testdata'; import {DEFAULT_SOURCE, URL, STATUSES} from './UsersHelper'; -const {singleUserUrl, createUserUrl, defaultRolesUrl, findUsersUrl, changePasswordUrl} = URL; - -const {USERS: {FORM: LABELS, MODAL}, SETTINGS} = UIStrings; +const { + singleUserUrl, + createUserUrl, + defaultRolesUrl, + findUsersUrl, + changePasswordUrl, + resetTokenUrl +} = URL; + +const { + USERS: { + FORM: LABELS, + MODAL, + TOKEN + }, + SETTINGS +} = UIStrings; jest.mock('axios', () => ({ ...jest.requireActual('axios'), @@ -53,6 +67,7 @@ jest.mock('@sonatype/nexus-ui-plugin', () => ({ showSuccessMessage: jest.fn(), state: jest.fn().mockReturnValue({ getValue: jest.fn(), + getEdition: jest.fn(), getUser: jest.fn(), }), }, @@ -120,6 +135,20 @@ const selectors = { querySave: () => queryByText(selectors.modal.container(), SETTINGS.SAVE_BUTTON_LABEL), retryButton: () => screen.queryByText('Retry'), }, + token: { + container: () => screen.getByLabelText(TOKEN.RESET_USER_TOKEN), + queryModal: () => screen.queryByLabelText(TOKEN.RESET_USER_TOKEN), + cancel: () => getByText(selectors.token.container(), SETTINGS.CANCEL_BUTTON_LABEL), + title: () => screen.getByText(TOKEN.LABEL), + queryTitle: () => screen.queryByText(TOKEN.LABEL), + link: () => screen.getByText('capabilities page'), + queryResetButton: () => screen.queryByText(TOKEN.RESET_USER_TOKEN), + resetButton: () => screen.getByText(TOKEN.RESET_USER_TOKEN, {ignore: 'span'}), + activeLabel: () => screen.getByText(TOKEN.TEXT), + modalText: () => screen.getByText(TOKEN.ACTIVE_FEATURE), + authenticate: () => screen.getByText(TOKEN.AUTHENTICATE), + queryAuthenticate: () => screen.queryByText(TOKEN.AUTHENTICATE), + } }; const shouldSeeDetailsInReadOnlyMode = ({statusValue = testStatus} = {}) => { @@ -549,7 +578,11 @@ describe('UsersDetails', function() { Promise.resolve({ data: 'fakeToken', success: true }) ); - ExtJS.state().getUser.mockReturnValue({id: 'admin'}); + ExtJS.state = jest.fn().mockReturnValue({ + getUser: jest.fn().mockReturnValue({id: 'admin'}), + getEdition: jest.fn().mockReturnValue('PRO'), + getValue: jest.fn() + }); }); it('renders correctly', async () => { @@ -689,11 +722,18 @@ describe('UsersDetails', function() { }); it('closes modal when pressing cancel button', async () => { + ExtJS.fetchAuthenticationToken = jest.fn(() => + Promise.resolve({ data: 'fakeToken', success: true }) + ); + const { openButton, queryModal, container, - cancel + cancel, + next, + inputAdminPassword, + save } = selectors.modal; await renderAndWaitForLoad(testId); @@ -708,6 +748,21 @@ describe('UsersDetails', function() { await userEvent.click(cancel()); expect(queryModal()).not.toBeInTheDocument(); + + await userEvent.click(openButton()); + + await TestUtils.changeField(inputAdminPassword, 'admin123'); + + await userEvent.click(next()); + + await waitFor(() => { + expect(cancel()).toBeInTheDocument(); + expect(save()).toBeInTheDocument(); + }); + + await userEvent.click(cancel()); + + expect(queryModal()).not.toBeInTheDocument(); }); @@ -787,4 +842,154 @@ describe('UsersDetails', function() { }); }); }); + + describe('Reset Token', () => { + beforeEach(() => { + ExtJS.state = jest.fn().mockReturnValue({ + getUser: jest.fn().mockReturnValue({id: 'admin'}), + getEdition: jest.fn().mockReturnValue('PRO'), + getValue: jest.fn().mockReturnValue(['usertoken']) + }); + }); + + it('renders correctly', async () => { + ExtJS.state = jest.fn().mockReturnValue({ + getUser: jest.fn().mockReturnValue({id: 'admin'}), + getEdition: jest.fn().mockReturnValue('PRO'), + getValue: jest.fn(), + }); + + const { + title, + link, + queryResetButton, + } = selectors.token; + + await renderAndWaitForLoad(testId); + + expect(title()).toBeInTheDocument(); + expect(link()).toBeInTheDocument(); + expect(queryResetButton()).not.toBeInTheDocument(); + }); + + it('does not show reset token user section if it is not nexus pro', async () => { + const {queryTitle, queryResetButton} = selectors.token; + + ExtJS.state = jest.fn().mockReturnValue({ + getUser: jest.fn().mockReturnValue({id: 'admin'}), + getEdition: jest.fn().mockReturnValue('FREE'), + getValue: jest.fn(), + }); + + await renderAndWaitForLoad(testId); + + expect(queryTitle()).not.toBeInTheDocument(); + expect(queryResetButton()).not.toBeInTheDocument(); + }); + + it('show reset button if the capability is enabled', async () => { + const {resetButton} = selectors.token; + + await renderAndWaitForLoad(testId); + + expect(resetButton()).toBeInTheDocument(); + }); + + it('requires authentication to be able to reset the token', async () => { + const { + token: { + resetButton, + activeLabel, + modalText, + authenticate + }, + modal: { + inputAdminPassword + } + } = selectors; + + ExtJS.fetchAuthenticationToken = jest.fn(() => + Promise.resolve({ data: "Invalid", success: false, message: 'Authentication failed' }) + ); + + await renderAndWaitForLoad(testId); + + await userEvent.click(resetButton()); + + expect(activeLabel()).toBeInTheDocument(); + expect(modalText()).toBeInTheDocument(); + expect(authenticate()).toBeInTheDocument(); + + await TestUtils.changeField(inputAdminPassword, 'incorrect'); + await userEvent.click(authenticate()); + + await waitFor(() => { + expect(inputAdminPassword()).toHaveErrorMessage('Authentication failed'); + }); + }); + + it('reset button is disabled if the user does not have enough permissions', async () => { + const {resetButton} = selectors.token; + + when(ExtJS.checkPermission) + .calledWith('nexus:usertoken-settings:update').mockReturnValue(false); + + await renderAndWaitForLoad(testId); + + expect(resetButton()).toBeDisabled(); + }); + + it('reset token properly', async () => { + when(Axios.delete) + .calledWith(resetTokenUrl(testId, 'default')) + .mockResolvedValue(); + + ExtJS.fetchAuthenticationToken = jest.fn(() => + Promise.resolve({ data: 'fakeToken', success: true }) + ); + + const { + token: { + resetButton, + authenticate, + queryAuthenticate + }, + modal: { + inputAdminPassword + } + } = selectors; + + await renderAndWaitForLoad(testId); + + expect(resetButton()).not.toBeDisabled(); + + await userEvent.click(resetButton()); + + await TestUtils.changeField(inputAdminPassword, 'admin123'); + + await act(async () => userEvent.click(authenticate())); + + await waitFor(() => { + expect(Axios.delete).toHaveBeenCalledTimes(1); + expect(Axios.delete).toHaveBeenCalledWith(resetTokenUrl(testId, 'default')); + expect(queryAuthenticate()).not.toBeInTheDocument(); + }); + }); + + it('closes modal when pressing cancel button', async () => { + const {resetButton, cancel, queryModal} = selectors.token; + + await renderAndWaitForLoad(testId); + + expect(resetButton()).not.toBeDisabled(); + + await userEvent.click(resetButton()); + + expect(cancel()).toBeInTheDocument(); + + await userEvent.click(cancel()); + + expect(queryModal()).not.toBeInTheDocument(); + }); + }); }); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersForm.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersForm.jsx index 702c5cf757..40d10eb2e6 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersForm.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersForm.jsx @@ -232,7 +232,12 @@ export default function UsersForm({service, onDone}) { aria-labelledby="modal-form-header" variant="narrow"> {confirmingAdminPassword && - + } {confirmingNewPassword && diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersFormMachine.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersFormMachine.js index cf8ccbda60..11a7b1a419 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersFormMachine.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersFormMachine.js @@ -28,6 +28,7 @@ import UIStrings from '../../../../constants/UIStrings'; import {EMPTY_DATA, URL, fullName} from './UsersHelper'; import confirmAdminPasswordMachine from './confirmAdminPasswordMachine'; import confirmNewPasswordMachine from './confirmNewPasswordMachine'; +import resettingTokenMachine from './resettingTokenMachine'; const {USERS: {MESSAGES: LABELS}} = UIStrings; @@ -50,7 +51,8 @@ export default FormUtils.buildFormMachine({ loaded: { id: 'loaded', on: { - CHANGE_PASSWORD: 'changingPassword' + CHANGE_PASSWORD: 'changingPassword', + RESET_TOKEN: 'resetToken', } }, changingPassword: { @@ -85,7 +87,38 @@ export default FormUtils.buildFormMachine({ }, on: { - CANCEL_CHANGE_PASSWORD: '#loaded' + CANCEL: '#loaded' + } + }, + resetToken: { + initial: 'confirmAdminPassword', + + states: { + confirmAdminPassword: { + invoke: { + id: 'confirmAdminPasswordMachine', + src: 'confirmAdminPasswordMachine', + onDone: 'resetting' + } + }, + resetting: { + invoke: { + id: 'resettingTokenMachine', + src: 'resettingTokenMachine', + data: ({data}) => ({ + data: { + userId: data.userId, + source: data.source, + name: data.firstName + } + }), + onDone: '#loaded' + } + } + }, + + on: { + CANCEL: '#loaded' } } } @@ -146,6 +179,7 @@ export default FormUtils.buildFormMachine({ delete: ({data}) => Axios.delete(singleUserUrl(data.userId)), confirmAdminPasswordMachine, - confirmNewPasswordMachine + confirmNewPasswordMachine, + resettingTokenMachine } }); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersHelper.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersHelper.js index 58af2d3ae6..bf547f9f91 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersHelper.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersHelper.js @@ -48,6 +48,7 @@ const createUserUrl = usersUrl; const getRolesUrl = (source) => `${rolesUrl}?source=${encodeURIComponent(source)}`; const defaultRolesUrl = getRolesUrl('default'); const changePasswordUrl = (userId) => `${usersUrl}/${encodeURIComponent(userId)}/change-password`; +const resetTokenUrl = (userId, realm) => `${usersUrl}/${encodeURIComponent(userId)}/${realm}/user-token-reset`; export const URL = { usersUrl, @@ -55,7 +56,8 @@ export const URL = { createUserUrl, defaultRolesUrl, findUsersUrl, - changePasswordUrl + changePasswordUrl, + resetTokenUrl }; export const isAnonymousUser = (userId) => userId === ExtJS.state().getValue('anonymousUsername'); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersToken.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersToken.jsx new file mode 100644 index 0000000000..8713b35762 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/UsersToken.jsx @@ -0,0 +1,99 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import React from 'react'; +import {useService} from '@xstate/react'; +import { + NxH2, + NxTile, + NxButton, + NxButtonBar, + NxFieldset, + NxFormRow, + NxModal, + NxTooltip, +} from '@sonatype/react-shared-components'; + +import UIStrings from '../../../../constants/UIStrings'; + +import {ExtJS} from '@sonatype/nexus-ui-plugin'; +const { + USERS: {TOKEN: LABELS}, +} = UIStrings; + +import ConfirmAdminPasswordForm from './ConfirmAdminPasswordForm'; + +export default function UsersToken({service}) { + const [current, send] = useService(service); + const activeCapabilities = + ExtJS.state().getValue('capabilityActiveTypes') || []; + const isCapabilityActive = activeCapabilities.includes('usertoken'); + const confirmingAdminPassword = current.matches( + 'resetToken.confirmAdminPassword' + ); + const canReset = ExtJS.checkPermission('nexus:usertoken-settings:update'); + + const reset = () => send('RESET_TOKEN'); + + return ( + <> + + + {LABELS.LABEL} + + + + + + {isCapabilityActive && ( + + + + {LABELS.RESET_USER_TOKEN} + + + + )} + + + + {confirmingAdminPassword && ( + + + + )} + + ); +} diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmAdminPasswordMachine.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmAdminPasswordMachine.js index 0ce4086bd2..81a808d9e9 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmAdminPasswordMachine.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmAdminPasswordMachine.js @@ -44,7 +44,7 @@ export default FormUtils.buildFormMachine({ on: { CANCEL: { - actions: sendParent('CANCEL_CHANGE_PASSWORD'), + actions: sendParent('CANCEL'), }, }, }), diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmNewPasswordMachine.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmNewPasswordMachine.js index 0c16f24c7a..89f78ffb60 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmNewPasswordMachine.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/confirmNewPasswordMachine.js @@ -41,7 +41,7 @@ export default FormUtils.buildFormMachine({ }, on: { CANCEL: { - actions: sendParent('CANCEL_CHANGE_PASSWORD'), + actions: sendParent('CANCEL'), }, }, }), diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/resettingTokenMachine.js b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/resettingTokenMachine.js new file mode 100644 index 0000000000..21108ea5f5 --- /dev/null +++ b/plugins/nexus-coreui-plugin/src/frontend/src/components/pages/admin/Users/resettingTokenMachine.js @@ -0,0 +1,55 @@ +/* + * Sonatype Nexus (TM) Open Source Version + * Copyright (c) 2008-present Sonatype, Inc. + * All rights reserved. Includes the third-party code listed at http://links.sonatype.com/products/nexus/oss/attributions. + * + * This program and the accompanying materials are made available under the terms of the Eclipse Public License Version 1.0, + * which accompanies this distribution and is available at http://www.eclipse.org/legal/epl-v10.html. + * + * Sonatype Nexus (TM) Open Source Version is distributed with Sencha Ext JS pursuant to a FLOSS Exception agreed upon + * between Sonatype, Inc. and Sencha Inc. Sencha Ext JS is licensed under GPL v3 and cannot be redistributed as part of a + * closed source work. + * + * Sonatype Nexus (TM) Professional Version is available from Sonatype, Inc. "Sonatype" and "Sonatype Nexus" are trademarks + * of Sonatype, Inc. Apache Maven is a trademark of the Apache Software Foundation. M2eclipse is a trademark of the + * Eclipse Foundation. All other trademarks are the property of their respective owners. + */ +import {sendParent} from 'xstate'; +import {mergeDeepRight} from 'ramda'; +import Axios from 'axios'; +import {ExtJS, FormUtils} from '@sonatype/nexus-ui-plugin'; +import {URL} from './UsersHelper'; +import UIStrings from '../../../../constants/UIStrings'; + +const { + USERS: {TOKEN: LABELS}, +} = UIStrings; + +export default FormUtils.buildFormMachine({ + id: 'resettingTokenMachine', + initial: 'saving', + + stateAfterSave: 'done', + config: (config) => + mergeDeepRight(config, { + states: { + done: { + type: 'final', + }, + }, + on: { + CANCEL: { + actions: sendParent('CANCEL'), + }, + }, + }), +}).withConfig({ + actions: { + logSaveSuccess: ({data}) => + ExtJS.showSuccessMessage(LABELS.SAVE_SUCCESS(data.name)), + }, + services: { + saveData: ({data}) => + Axios.delete(URL.resetTokenUrl(data.userId, data.source)), + }, +}); diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/constants/UIStrings.jsx b/plugins/nexus-coreui-plugin/src/frontend/src/constants/UIStrings.jsx index d30a252f65..12b4825d20 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/constants/UIStrings.jsx +++ b/plugins/nexus-coreui-plugin/src/frontend/src/constants/UIStrings.jsx @@ -209,7 +209,7 @@ export default { USERS: { MENU: { text: 'Users', - description: 'Manage Users' + description: 'Manage users', }, MODAL: { TEXT: 'You must confirm your current password before you are able to change or update the password', @@ -304,6 +304,19 @@ export default { }, DELETE_SUCCESS: (name) => `User deleted: ${name}`, }, + TOKEN: { + LABEL: 'User Token Options', + USER_TOKEN: 'User Token', + ACTIVE_FEATURE: 'Resetting a user’s token will invalidate their current token and force a new token to be created the next time it is accessed by the user', + REQUIRE_ENABLE_FEATURE: <> + User tokens allow users to authenticate securely without typical user credentials such as those used by LDAP or Crowd. To use this feature, visit the + capabilities page and create a “User Token” capability. + , + RESET_USER_TOKEN: 'Reset user token', + TEXT: 'Resetting user tokens requires validation of your credentials.', + AUTHENTICATE: 'Authenticate', + SAVE_SUCCESS: (user) => `User token of ${user} has been reset` + } }, SSL_CERTIFICATES: { @@ -433,6 +446,27 @@ export default { }, }, }, + VERIFY: { + LABEL: 'Verify Email Server', + SUB_LABEL: 'Where do you want to send the test email?', + TEST: 'Test', + SUCCESS: 'Email server verification email sent successfully', + ERROR: 'Email server verification email failed' + }, + READ_ONLY: { + ENABLE: { + ENABLE_STARTTLS: 'STARTTLS support enabled for insecure connections', + REQUIRE_STARTTLS: 'STARTTLS support required', + ENABLE_SSL_TLS: 'SSL/TLS encryption enabled upon connection', + IDENTITY_CHECK: 'Server identity check enabled', + }, + NOT_ENABLE: { + ENABLE_STARTTLS: 'STARTTLS support not enabled for insecure connections', + REQUIRE_STARTTLS: 'STARTTLS support not required', + ENABLE_SSL_TLS: 'SSL/TLS encryption not enabled upon connection', + IDENTITY_CHECK: 'Server identity check not enabled', + } + } }, BLOB_STORES: { @@ -870,6 +904,27 @@ export default { CONTENT_DISPOSITION_SUBLABEL: 'Add Content-Disposition header as "Attachment" to disable some content from being inline in a browser', VERSION_POLICY_LABEL: 'Version Policy', VERSION_POLICY_SUBLABEL: 'What type of artifacts does this repository store?', + APT: { + CAPTION: 'APT Settings', + DISTRIBUTION: { + LABEL: 'Distribution', + SUBLABEL: 'Distribution to fetch (e.g., bionic)' + }, + FLAT: { + LABEL: 'Flat', + DESCR: 'Is this repository flat?' + }, + SIGNING: { + KEY: { + LABEL: 'Signing key', + SUBLABEL: 'PGP signing key pair (armored private key e.g., gpg --export-secret-key --armor)', + PLACEHOLDER: 'Entry' + }, + PASSPHRASE: { + LABEL: 'Passphrase' + } + } + }, NPM: { REMOVE_NON_CATALOGED: { LABEL: 'Remove Non-catalogued Versions', @@ -884,14 +939,6 @@ export default { }, }, LEARN_MORE: 'Learn more', - REPOSITORY_CONNECTORS_CAPTION: 'Repository Connectors', - HTTP_CONNECTOR_LABEL: 'HTTP', - HTTP_CONNECTOR_SUBLABEL: 'Create an HTTP connector at specified port. Normally used if the server is behind a secure proxy', - HTTPS_CONNECTOR_LABEL: 'HTTPS', - HTTPS_CONNECTOR_SUBLABEL: 'Create an HTTP connector at specified port. Normally used if the server is configured for https', - DOCKER_CONNECTOR_PLACEHOLDER: 'Enter a port number', - ALLOW_ANON_DOCKER_PULL_LABEL: 'Allow anonymous docker pull', - ALLOW_ANON_DOCKER_PULL_DESCR: 'Allow anonymous docker pull (Docker Bearer Token Realm required)', REGISTRY_API_SUPPORT_CAPTION: 'Docker Registry API Support', REGISTRY_API_SUPPORT_LABEL: 'Enable Docker V1 API', REGISTRY_API_SUPPORT_DESCR: 'Allow clients to use the V1 API to interact with this repository', @@ -907,6 +954,51 @@ export default { LABEL: 'Location of the Docker Index', PLACEHOLDER: 'Enter a URL' } + }, + CONNECTORS: { + CAPTION: 'Repository Connectors', + HTTP: { + LABEL: 'HTTP', + SUBLABEL: 'Create an HTTP connector at specified port. Normally used if the server is behind a secure proxy', + PLACEHOLDER: 'Enter a port number' + }, + HTTPS: { + LABEL: 'HTTPS', + SUBLABEL: 'Create an HTTP connector at specified port. Normally used if the server is configured for https', + PLACEHOLDER: 'Enter a port number' + }, + SUBDOMAIN: { + LABEL: 'Allow Subdomain Routing', + SUBLABEL: 'Use the following subdomain to make push and pull requests for this repository', + PLACEHOLDER: 'Enter a subdomain', + VALIDATION_ERROR: 'Subdomain field must be a minimum of 1 and maximum of 63 characters (letters, numbers, and dashes) and must start with a letter and end with a letter or digit' + }, + ALLOW_ANON_DOCKER_PULL: { + LABEL: 'Allow anonymous docker pull', + DESCR: 'Allow anonymous docker pull (Docker Bearer Token Realm required)' + }, + HELP: <> + Connectors allow Docker clients to connect directly to hosted registries, but are not always + required. +
+ Consult our{' '} + + documentation + + {' '}for which connector is appropriate for your use case. +
+ For information on scaling see our{' '} + + scaling documentation + + . + , } }, FOREIGN_LAYER: { @@ -928,28 +1020,6 @@ export default { yum: ' (e.g., http://mirror.centos.org/centos/)', default: ' (e.g., https://example.com)' }, - DOCKER_CONNECTORS_HELP: <> - Connectors allow Docker clients to connect directly to hosted registries, but are not always - required. -
- Consult our{' '} - - documentation - - {' '}for which connector is appropriate for your use case. -
- For information on scaling see our{' '} - - scaling documentation - - . - , NUGET: { PROTOCOL_VERSION: { LABEL: 'Protocol Version', diff --git a/plugins/nexus-coreui-plugin/src/frontend/src/index.js b/plugins/nexus-coreui-plugin/src/frontend/src/index.js index 7cbe621503..34be4a6197 100644 --- a/plugins/nexus-coreui-plugin/src/frontend/src/index.js +++ b/plugins/nexus-coreui-plugin/src/frontend/src/index.js @@ -82,7 +82,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/Repository/Repositories-new', + path: '/Repository/Repositories', ...UIStrings.REPOSITORIES.MENU, view: Repositories, iconCls: 'x-fa fa-database', @@ -118,7 +118,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/Security/Privileges-New', + path: '/Security/Privileges', ...UIStrings.PRIVILEGES.MENU, view: Privileges, iconCls: 'x-fa fa-id-badge', @@ -131,7 +131,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/System/EmailServer-New', + path: '/System/EmailServer', ...UIStrings.EMAIL_SERVER.MENU, view: EmailServer, iconCls: 'x-fa fa-envelope', @@ -155,7 +155,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/Security/SslCertificates-New', + path: '/Security/SslCertificates', ...UIStrings.SSL_CERTIFICATES.MENU, view: SslCertificates, iconCls: 'x-fa fa-id-card-alt', @@ -167,7 +167,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/Security/Users-New', + path: '/Security/Users', ...UIStrings.USERS.MENU, view: Users, iconCls: 'x-fa fa-users', @@ -366,7 +366,7 @@ window.plugins.push({ }, { mode: 'admin', - path: '/System/HTTP-new', + path: '/System/HTTP', ...UIStrings.HTTP.MENU, view: HTTP, iconCls: 'x-fa fa-truck', diff --git a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/FeatureGroups.js b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/FeatureGroups.js index e481ddadb0..3a0daeaaf7 100644 --- a/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/FeatureGroups.js +++ b/plugins/nexus-coreui-plugin/src/main/resources/static/rapture/NX/coreui/controller/FeatureGroups.js @@ -81,17 +81,6 @@ Ext.define('NX.coreui.controller.FeatureGroups', { variants: ['x16', 'x32'] } }, - { - mode: 'browse', - path: '/Upload', - text: NX.I18n.get('FeatureGroups_Upload_Text'), - description: NX.I18n.get('FeatureGroups_Upload_Description'), - group: true, - iconConfig: { - file: 'upload.png', - variants: ['x16', 'x32'] - } - } ]); } }); diff --git a/plugins/nexus-repository-apt/src/main/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapter.java b/plugins/nexus-repository-apt/src/main/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapter.java index 2c834bd2e2..7d50a61b90 100644 --- a/plugins/nexus-repository-apt/src/main/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapter.java +++ b/plugins/nexus-repository-apt/src/main/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapter.java @@ -76,7 +76,7 @@ private AptHostedRepositoriesAttributes createAptHostedRepositoriesAttributes(fi } private AptSigningRepositoriesAttributes createAptSigningRepositoriesAttributes(final Repository repository) { - NestedAttributesMap aptAttributes = repository.getConfiguration().attributes(AptFormat.NAME); + NestedAttributesMap aptAttributes = repository.getConfiguration().attributes("aptSigning"); String keypair = aptAttributes.get("keypair", String.class); String passphrase = aptAttributes.get("passphrase", String.class); if (!Strings2.isBlank(passphrase)) { diff --git a/plugins/nexus-repository-apt/src/test/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapterTest.java b/plugins/nexus-repository-apt/src/test/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapterTest.java index b50ad2ffb6..67ba0d9af0 100644 --- a/plugins/nexus-repository-apt/src/test/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapterTest.java +++ b/plugins/nexus-repository-apt/src/test/java/org/sonatype/nexus/repository/apt/api/AptApiRepositoryAdapterTest.java @@ -37,7 +37,7 @@ import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.AdditionalMatchers.not; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.startsWith; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -65,7 +65,7 @@ public void testAdapt_hostedRepository() throws Exception { assertThat(hostedRepository.getAptSigning(), nullValue()); // only include public key if it is encrypted - repository.getConfiguration().attributes("apt").set("passphrase", "mypass"); + repository.getConfiguration().attributes("aptSigning").set("passphrase", "mypass"); hostedRepository = (AptHostedApiRepository) underTest.adapt(repository); assertThat(hostedRepository.getAptSigning().getKeypair(), is("asdf")); assertThat(hostedRepository.getAptSigning().getPassphrase(), nullValue()); @@ -97,7 +97,7 @@ private static Configuration config(final String repositoryName) { Configuration configuration = mock(Configuration.class); when(configuration.isOnline()).thenReturn(true); when(configuration.getRepositoryName()).thenReturn(repositoryName); - when(configuration.attributes(not(eq("apt")))).thenReturn(new NestedAttributesMap("dummy", newHashMap())); + when(configuration.attributes(not(startsWith("apt")))).thenReturn(new NestedAttributesMap("dummy", newHashMap())); return configuration; } @@ -111,15 +111,22 @@ private static Repository createRepository( Repository repository = new RepositoryImpl(mock(EventManager.class), type, new AptFormat()); Configuration configuration = config("my-repo"); + Map attributes = newHashMap(); + Map apt = newHashMap(); apt.put("distribution", distribution); - apt.put("keypair", keypair); - apt.put("passphrase", passphrase); apt.put("flat", flat); - Map attributes = newHashMap(); attributes.put("apt", apt); NestedAttributesMap aptNested = new NestedAttributesMap("apt", apt); when(configuration.attributes("apt")).thenReturn(aptNested); + + Map aptSigning = newHashMap(); + aptSigning.put("keypair", keypair); + aptSigning.put("passphrase", passphrase); + attributes.put("aptSigning", aptSigning); + NestedAttributesMap aptSigningNested = new NestedAttributesMap("aptSigning", aptSigning); + when(configuration.attributes("aptSigning")).thenReturn(aptSigningNested); + repository.init(configuration); return repository; } diff --git a/revision.txt b/revision.txt index e8f4255b78..9c8c98ed51 100644 --- a/revision.txt +++ b/revision.txt @@ -1 +1 @@ -b=main,r=7cb7558bdbc4cb5287779e87ad64586a8bb3cca0,t=2022-09-23-1242-37227 \ No newline at end of file +b=main,r=314d894c671a662a70e9c0f93c105a2b90765ca0,t=2022-09-30-1242-43906 \ No newline at end of file diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/FormatRepositoryTestSystemSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/FormatRepositoryTestSystemSupport.java index 17d1e5d7bb..ec3e5dc111 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/FormatRepositoryTestSystemSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/FormatRepositoryTestSystemSupport.java @@ -89,6 +89,10 @@ public abstract class FormatRepositoryTestSystemSupport public static final String ATTRIBUTES_KEY_REPLICATION_ENABLED = "enabled"; + public static final String ATTRIBUTES_KEY_PULL_REPLICATION_ENABLED = "preemptivePullEnabled"; + + public static final String ATTRIBUTES_ASSET_PATH_REGEX = "assetPathRegex"; + private final RepositoryManager repositoryManager; private Consumer tracker; @@ -167,9 +171,8 @@ private Configuration applyHostedAttributes(Configuration configuration, HOSTED private Configuration applyProxyAttributes(Configuration configuration, PROXY config) { NestedAttributesMap httpclient = configuration.attributes(ATTRIBUTES_MAP_KEY_HTTPCLIENT); - addToMapCreateIfNeeded(httpclient, ATTRIBUTES_MAP_KEY_CONNECTION, ATTRIBUTES_KEY_BLOCKED, config.isBlocked()); - addToMapCreateIfNeeded(httpclient, ATTRIBUTES_MAP_KEY_CONNECTION, ATTRIBUTES_KEY_AUTO_BLOCKED, - config.isAutoBlocked()); + addConfigIfNotNull(httpclient, ATTRIBUTES_KEY_BLOCKED, config.isBlocked()); + addConfigIfNotNull(httpclient, ATTRIBUTES_KEY_AUTO_BLOCKED, config.isAutoBlocked()); addToMapCreateIfNeeded(httpclient, ATTRIBUTES_MAP_KEY_AUTHENTICATION, ATTRIBUTES_KEY_USERNAME, config.getUsername()); @@ -187,6 +190,10 @@ private Configuration applyProxyAttributes(Configuration configuration, PROXY co addConfigIfNotNull(negativeCache, ATTRIBUTES_KEY_NC_ENABLED, config.isNegativeCacheEnabled()); addConfigIfNotNull(negativeCache, ATTRIBUTES_KEY_NC_TTL, config.getNegativeCacheTimeToLive()); + NestedAttributesMap replication = configuration.attributes(ATTRIBUTES_MAP_KEY_REPLICATION); + addConfigIfNotNull(replication, ATTRIBUTES_KEY_PULL_REPLICATION_ENABLED, config.isPreemptivePullEnabled()); + addConfigIfNotNull(replication, ATTRIBUTES_ASSET_PATH_REGEX, config.getAssetPathRegex()); + return configuration; } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/GolangFormatRepositoryTestSystem.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/GolangFormatRepositoryTestSystem.java index e06641199f..d019661b2d 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/GolangFormatRepositoryTestSystem.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/GolangFormatRepositoryTestSystem.java @@ -34,6 +34,7 @@ public class GolangFormatRepositoryTestSystem { @Inject public GolangFormatRepositoryTestSystem(final RepositoryManager repositoryManager) { - super(repositoryManager); + super(repositoryManager, GolangHostedRepositoryConfig.class, GolangProxyRepositoryConfig.class, + GolangGroupRepositoryConfig.class); } } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/MavenFormatRepositoryTestSystem.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/MavenFormatRepositoryTestSystem.java index 7339e741be..c1f7946eb0 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/MavenFormatRepositoryTestSystem.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/MavenFormatRepositoryTestSystem.java @@ -37,56 +37,45 @@ public class MavenFormatRepositoryTestSystem MavenGroupRepositoryConfig> implements FormatRepositoryTestSystem { - public static final String ATTRIBUTES_MAP_KEY_AUTHENTICATION = "authentication"; - public static final String ATTRIBUTES_MAP_KEY_MAVEN = "maven"; - public static final String ATTRIBUTES_MAP_KEY_REPLICATION = "replication"; - - public static final String ATTRIBUTES_KEY_PULL_REPLICATION_ENABLED = "preemptivePullEnabled"; - - public static final String ATTRIBUTES_ASSET_PATH_REGEX = "assetPathRegex"; - public static final String ATTRIBUTES_KEY_VERSION_POLICY = "versionPolicy"; public static final String ATTRIBUTES_KEY_LAYOUT_POLICY = "layoutPolicy"; - public static final String ATTRIBUTES_KEY_USERNAME = "username"; - - public static final String ATTRIBUTES_KEY_PASSWORD = "password"; - @Inject public MavenFormatRepositoryTestSystem(final RepositoryManager repositoryManager) { super(repositoryManager); } - public Repository createHosted(final MavenHostedRepositoryConfig config) throws Exception { + public MavenHostedRepositoryConfig hosted(final String name) { + return new MavenHostedRepositoryConfig(this::createHosted).withName(name); + } + + public Repository createHosted(final MavenHostedRepositoryConfig config) { return doCreate( applyMavenAttributes(createHostedConfiguration(config), config.getVersionPolicy(), config.getLayoutPolicy())); } - public Repository createProxy(final MavenProxyRepositoryConfig config) throws Exception { - Configuration cfg = applyMavenAttributes(createProxyConfiguration(config), config.getVersionPolicy(), config.getLayoutPolicy()); - applyAuthenticationAttributes(cfg, config.getUsername(), config.getPassword()); - applyPullReplicationAttributes(cfg, config.isPreemptivePullEnabled(), config.getAssetPathRegex()); + public MavenProxyRepositoryConfig proxy(final String name) { + return new MavenProxyRepositoryConfig(this::createProxy) + .withName(name); + } + + public Repository createProxy(final MavenProxyRepositoryConfig config) { + Configuration cfg = + applyMavenAttributes(createProxyConfiguration(config), config.getVersionPolicy(), config.getLayoutPolicy()); return doCreate(cfg); } - public Repository createGroup(final MavenGroupRepositoryConfig config) throws Exception { - return doCreate( - applyMavenAttributes(createGroupConfiguration(config), config.getVersionPolicy(), config.getLayoutPolicy())); + public MavenGroupRepositoryConfig group(final String name) { + return new MavenGroupRepositoryConfig(this::createGroup) + .withName(name); } - private Configuration applyAuthenticationAttributes( - final Configuration configuration, - final String username, - final String password - ) - { - NestedAttributesMap authentication = configuration.attributes( ATTRIBUTES_MAP_KEY_AUTHENTICATION); - addConfigIfNotNull(authentication, ATTRIBUTES_KEY_USERNAME, username); - addConfigIfNotNull(authentication, ATTRIBUTES_KEY_PASSWORD, password); - return configuration; + public Repository createGroup(final MavenGroupRepositoryConfig config) { + return doCreate( + applyMavenAttributes(createGroupConfiguration(config), config.getVersionPolicy(), config.getLayoutPolicy())); } private Configuration applyMavenAttributes( @@ -99,17 +88,4 @@ private Configuration applyMavenAttributes( addConfigIfNotNull(maven, ATTRIBUTES_KEY_LAYOUT_POLICY, layoutPolicy); return configuration; } - - private Configuration applyPullReplicationAttributes( - final Configuration configuration, - final boolean replicationEnabled, - final String assetPathRegex - ) - { - NestedAttributesMap replication = configuration.attributes(ATTRIBUTES_MAP_KEY_REPLICATION); - addConfigIfNotNull(replication, ATTRIBUTES_KEY_PULL_REPLICATION_ENABLED, replicationEnabled); - addConfigIfNotNull(replication, ATTRIBUTES_ASSET_PATH_REGEX, assetPathRegex); - - return configuration; - } } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RFormatRepositoryTestSystem.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RFormatRepositoryTestSystem.java index 4bf3ecf022..97782ed1e8 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RFormatRepositoryTestSystem.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RFormatRepositoryTestSystem.java @@ -34,6 +34,6 @@ public class RFormatRepositoryTestSystem { @Inject public RFormatRepositoryTestSystem(final RepositoryManager repositoryManager) { - super(repositoryManager); + super(repositoryManager, RHostedRepositoryConfig.class, RProxyRepositoryConfig.class, RGroupRepositoryConfig.class); } } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RawFormatRepositoryTestSystem.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RawFormatRepositoryTestSystem.java index 5be66f0fc2..c645d2bed4 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RawFormatRepositoryTestSystem.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/RawFormatRepositoryTestSystem.java @@ -34,6 +34,7 @@ public class RawFormatRepositoryTestSystem { @Inject public RawFormatRepositoryTestSystem(final RepositoryManager repositoryManager) { - super(repositoryManager); + super(repositoryManager, RawHostedRepositoryConfig.class, RawProxyRepositoryConfig.class, + RawGroupRepositoryConfig.class); } } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/SimpleFormatRepositoryTestSystemSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/SimpleFormatRepositoryTestSystemSupport.java index 102e56c802..53ac74939d 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/SimpleFormatRepositoryTestSystemSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/SimpleFormatRepositoryTestSystemSupport.java @@ -12,6 +12,8 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository; +import java.util.function.Function; + import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.manager.RepositoryManager; import org.sonatype.nexus.testsuite.testsupport.system.repository.config.GroupRepositoryConfig; @@ -24,19 +26,60 @@ public abstract class SimpleFormatRepositoryTestSystemSupport GROUP extends GroupRepositoryConfig> extends FormatRepositoryTestSystemSupport { - protected SimpleFormatRepositoryTestSystemSupport(final RepositoryManager repositoryManager) { + private Class hostedClass; + + private Class proxyClass; + + private Class groupClass; + + public SimpleFormatRepositoryTestSystemSupport( + final RepositoryManager repositoryManager, + final Class hostedClass, + final Class proxyClass, + final Class groupClass) + { super(repositoryManager); + this.hostedClass = hostedClass; + this.proxyClass = proxyClass; + this.groupClass = groupClass; } - public Repository createHosted(final HOSTED config) throws Exception { + public Repository createHosted(final HOSTED config) { return doCreate(createHostedConfiguration(config)); } - public Repository createProxy(final PROXY config) throws Exception { + public Repository createProxy(final PROXY config) { return doCreate(createProxyConfiguration(config)); } - public Repository createGroup(final GROUP config) throws Exception { + public Repository createGroup(final GROUP config) { return doCreate(createGroupConfiguration(config)); } + + @SuppressWarnings("unchecked") + public HOSTED hosted(final String name) { + return (HOSTED) create(hostedClass, this::createHosted) + .withName(name); + } + + @SuppressWarnings("unchecked") + public PROXY proxy(final String name) { + return (PROXY) create(proxyClass, this::createProxy) + .withName(name); + } + + @SuppressWarnings("unchecked") + public GROUP group(final String name) { + return (GROUP) create(groupClass, this::createGroup) + .withName(name); + } + + private static E create(final Class clazz, final Function factory) { + try { + return clazz.getConstructor(Function.class).newInstance(factory); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptHostedRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptHostedRepositoryConfig.java index c235d12b10..178cee6344 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptHostedRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptHostedRepositoryConfig.java @@ -12,6 +12,10 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_APT; public class AptHostedRepositoryConfig @@ -21,6 +25,10 @@ public class AptHostedRepositoryConfig private String keypair; + public AptHostedRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_APT; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptProxyRepositoryConfig.java index 089fd67a39..5faa84f464 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/AptProxyRepositoryConfig.java @@ -12,6 +12,10 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_APT; public class AptProxyRepositoryConfig @@ -21,6 +25,10 @@ public class AptProxyRepositoryConfig private Boolean flat; + public AptProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_APT; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CocoapodsProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CocoapodsProxyRepositoryConfig.java index cc38830f79..49915eb510 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CocoapodsProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CocoapodsProxyRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_COCOAPODS; public class CocoapodsProxyRepositoryConfig extends ProxyRepositoryConfigSupport { + public CocoapodsProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_COCOAPODS; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CondaProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CondaProxyRepositoryConfig.java index 478aef31fb..e2458aa4d8 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CondaProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/CondaProxyRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_CONDA; public class CondaProxyRepositoryConfig extends ProxyRepositoryConfigSupport { + public CondaProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_CONDA; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangGroupRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangGroupRepositoryConfig.java index 6f5e2c90a1..5acdb94b5d 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangGroupRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangGroupRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_GOLANG; public class GolangGroupRepositoryConfig extends GroupRepositoryConfigSupport { + public GolangGroupRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_GOLANG; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangHostedRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangHostedRepositoryConfig.java index 4c15cced99..b554382ef4 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangHostedRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangHostedRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_GOLANG; public class GolangHostedRepositoryConfig extends HostedRepositoryConfigSupport { + public GolangHostedRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_GOLANG; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangProxyRepositoryConfig.java index a1395d51a5..ad3710b0fa 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GolangProxyRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_GOLANG; public class GolangProxyRepositoryConfig extends ProxyRepositoryConfigSupport { + public GolangProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_GOLANG; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GroupRepositoryConfigSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GroupRepositoryConfigSupport.java index b7f4ee0778..6ed66e54f8 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GroupRepositoryConfigSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/GroupRepositoryConfigSupport.java @@ -12,6 +12,10 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + public abstract class GroupRepositoryConfigSupport extends RepositoryConfigSupport implements GroupRepositoryConfig @@ -22,6 +26,10 @@ public abstract class GroupRepositoryConfigSupport private String groupWriteMember = "None"; + public GroupRepositoryConfigSupport(final Function factory) { + super(factory); + } + @Override public String getRecipe() { return getFormat() + GROUP_RECIPE_SUFFIX; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/HostedRepositoryConfigSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/HostedRepositoryConfigSupport.java index de48204d13..f6ced16e1d 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/HostedRepositoryConfigSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/HostedRepositoryConfigSupport.java @@ -12,6 +12,9 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.config.WritePolicy; public abstract class HostedRepositoryConfigSupport @@ -23,9 +26,13 @@ public abstract class HostedRepositoryConfigSupport private WritePolicy writePolicy = WritePolicy.ALLOW; private Boolean latestPolicy = false; - + private Boolean replicationEnabled = false; + public HostedRepositoryConfigSupport(final Function factory) { + super(factory); + } + @Override public String getRecipe() { return getFormat() + HOSTED_RECIPE_SUFFIX; @@ -59,6 +66,7 @@ public THIS withReplicationEnabled(final Boolean replicationEnabled) { return toTHIS(); } + @Override public Boolean isReplicationEnabled() { return replicationEnabled; } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenGroupRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenGroupRepositoryConfig.java index 3fdc2590c7..1eb4d2d617 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenGroupRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenGroupRepositoryConfig.java @@ -12,6 +12,9 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.maven.LayoutPolicy; import org.sonatype.nexus.repository.maven.VersionPolicy; @@ -24,6 +27,14 @@ public class MavenGroupRepositoryConfig private LayoutPolicy layoutPolicy = LayoutPolicy.STRICT; + public MavenGroupRepositoryConfig() { + this(null); + } + + public MavenGroupRepositoryConfig(final Function repositoryFactory) { + super(repositoryFactory); + } + @Override public String getFormat() { return FORMAT_MAVEN; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenHostedRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenHostedRepositoryConfig.java index a3e6fb0808..d028b6eca5 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenHostedRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenHostedRepositoryConfig.java @@ -12,6 +12,9 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.maven.LayoutPolicy; import org.sonatype.nexus.repository.maven.VersionPolicy; @@ -24,6 +27,10 @@ public class MavenHostedRepositoryConfig private LayoutPolicy layoutPolicy = LayoutPolicy.STRICT; + public MavenHostedRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_MAVEN; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenProxyRepositoryConfig.java index 3b11114bf7..b86077cc6c 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/MavenProxyRepositoryConfig.java @@ -12,6 +12,9 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; import org.sonatype.nexus.repository.maven.LayoutPolicy; import org.sonatype.nexus.repository.maven.VersionPolicy; @@ -24,6 +27,10 @@ public class MavenProxyRepositoryConfig private LayoutPolicy layoutPolicy = LayoutPolicy.STRICT; + public MavenProxyRepositoryConfig(final Function repositoryFactory) { + super(repositoryFactory); + } + @Override public String getFormat() { return FORMAT_MAVEN; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/ProxyRepositoryConfigSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/ProxyRepositoryConfigSupport.java index 31ba5b117a..481d03f9fe 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/ProxyRepositoryConfigSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/ProxyRepositoryConfigSupport.java @@ -12,6 +12,10 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + public abstract class ProxyRepositoryConfigSupport extends RepositoryConfigSupport implements ProxyRepositoryConfig @@ -40,6 +44,10 @@ public abstract class ProxyRepositoryConfigSupport private String password; + public ProxyRepositoryConfigSupport(final Function factory) { + super(factory); + } + @Override public String getRecipe() { return getFormat() + PROXY_RECIPE_SUFFIX; @@ -68,7 +76,7 @@ public Boolean isPreemptivePullEnabled(){ } @Override - public THIS withAssetPathRegex(String assetPathRegex){ + public THIS withAssetPathRegex(final String assetPathRegex){ this.assetPathRegex = assetPathRegex; return toTHIS(); } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RGroupRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RGroupRepositoryConfig.java index 91a413544f..372d473ced 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RGroupRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RGroupRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_R; public class RGroupRepositoryConfig extends GroupRepositoryConfigSupport { + public RGroupRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_R; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RHostedRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RHostedRepositoryConfig.java index a93457bfe4..2807ab4cea 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RHostedRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RHostedRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_R; public class RHostedRepositoryConfig extends HostedRepositoryConfigSupport { + public RHostedRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_R; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RProxyRepositoryConfig.java index 712e327778..24c7070458 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RProxyRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_R; public class RProxyRepositoryConfig extends ProxyRepositoryConfigSupport { + public RProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_R; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawGroupRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawGroupRepositoryConfig.java index c896dea325..7a0bcf6395 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawGroupRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawGroupRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_RAW; public class RawGroupRepositoryConfig extends GroupRepositoryConfigSupport { + public RawGroupRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_RAW; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawHostedRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawHostedRepositoryConfig.java index 6751cea498..0098c98a66 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawHostedRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawHostedRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_RAW; public class RawHostedRepositoryConfig extends HostedRepositoryConfigSupport { + public RawHostedRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_RAW; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawProxyRepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawProxyRepositoryConfig.java index 033b4a2e34..5cb6a16594 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawProxyRepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RawProxyRepositoryConfig.java @@ -12,11 +12,19 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.testsuite.testsupport.system.RepositoryTestSystem.FORMAT_RAW; public class RawProxyRepositoryConfig extends ProxyRepositoryConfigSupport { + public RawProxyRepositoryConfig(final Function factory) { + super(factory); + } + @Override public String getFormat() { return FORMAT_RAW; diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfig.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfig.java index 61223b9514..c531345f2f 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfig.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfig.java @@ -12,6 +12,8 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import org.sonatype.nexus.repository.Repository; + public interface RepositoryConfig { THIS withName(final String name); @@ -37,4 +39,6 @@ public interface RepositoryConfig THIS withStrictContentTypeValidation(final Boolean strictContentTypeValidation); Boolean isStrictContentTypeValidation(); + + Repository create(); } diff --git a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfigSupport.java b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfigSupport.java index d0e5384d1f..fbe70118c8 100644 --- a/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfigSupport.java +++ b/testsuite/nexus-repository-testsupport/src/main/java/org/sonatype/nexus/testsuite/testsupport/system/repository/config/RepositoryConfigSupport.java @@ -12,6 +12,10 @@ */ package org.sonatype.nexus.testsuite.testsupport.system.repository.config; +import java.util.function.Function; + +import org.sonatype.nexus.repository.Repository; + import static org.sonatype.nexus.blobstore.api.BlobStoreManager.DEFAULT_BLOBSTORE_NAME; import static org.sonatype.nexus.datastore.api.DataStoreManager.DEFAULT_DATASTORE_NAME; @@ -28,6 +32,12 @@ public abstract class RepositoryConfigSupport private Boolean strictContentTypeValidation = true; + private Function factory; + + RepositoryConfigSupport(final Function factory) { + this.factory = factory; + } + @Override public THIS withName(final String name) { this.name = name; @@ -83,6 +93,11 @@ public Boolean isStrictContentTypeValidation() { return strictContentTypeValidation; } + @Override + public Repository create() { + return factory.apply(toTHIS()); + } + protected THIS toTHIS() { return (THIS) this; }