From d9402023732d9b43e8c7d2759ea6f74b2774b424 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 17 Mar 2026 17:54:00 +0500 Subject: [PATCH 01/41] upload, get and update rest endpoints --- opennms-config-jaxb/pom.xml | 4 + .../config/trapd/TrapdConfiguration.java | 3 + .../opennms/web/rest/v2/TrapdRestService.java | 159 ++++++++++++++++++ .../opennms/web/rest/v2/api/TrapdRestApi.java | 90 ++++++++++ .../web/rest/v2/model/TrapdConfigPayload.java | 98 +++++++++++ .../applicationContext-cxf-rest-v2.xml | 5 +- .../web/rest/v2/TrapdRestServiceIT.java | 119 +++++++++++++ .../applicationContext-rest-test.xml | 2 + 8 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java create mode 100644 opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java create mode 100644 opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java create mode 100644 opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java diff --git a/opennms-config-jaxb/pom.xml b/opennms-config-jaxb/pom.xml index d7728df68af6..98f42297f9fc 100644 --- a/opennms-config-jaxb/pom.xml +++ b/opennms-config-jaxb/pom.xml @@ -83,5 +83,9 @@ org.opennms.core.test-api.xml test + + com.fasterxml.jackson.core + jackson-annotations + diff --git a/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java index c74eba745f71..4e9f8e86ffc3 100644 --- a/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java +++ b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java @@ -31,6 +31,7 @@ import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; +import com.fasterxml.jackson.annotation.JsonIgnore; import org.opennms.core.xml.ValidateUsing; @@ -69,6 +70,7 @@ public class TrapdConfiguration implements Serializable { * keeps track of state for field: _snmpTrapPort */ @XmlTransient + @JsonIgnore private boolean hasSnmpTrapPort; /** @@ -114,6 +116,7 @@ public class TrapdConfiguration implements Serializable { * keeps track of state for field: _newSuspectOnTrap */ @XmlTransient + @JsonIgnore private boolean hasNewSuspectOnTrap; /** diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java new file mode 100644 index 000000000000..c990b0549fb6 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java @@ -0,0 +1,159 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.web.rest.v2; + +import java.io.InputStream; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.SecurityContext; + +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.opennms.core.xml.JaxbUtils; +import org.opennms.features.config.exception.ValidationException; +import org.opennms.netmgt.config.trapd.TrapdConfiguration; +import org.opennms.netmgt.dao.api.TrapdConfigDao; +import org.opennms.web.rest.v2.api.TrapdRestApi; +import org.opennms.web.rest.v2.model.TrapdConfigPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class TrapdRestService implements TrapdRestApi { + + private static final Logger LOG = LoggerFactory.getLogger(TrapdRestService.class); + + @Autowired + private TrapdConfigDao m_trapdConfigDao; + + @Override + public Response uploadTrapdConfiguration(final Attachment attachment, final SecurityContext securityContext) { + if (attachment == null) { + return Response.status(Status.BAD_REQUEST).entity("Missing uploaded file field 'upload'.").build(); + } + + final TrapdConfiguration config; + try (InputStream inputStream = attachment.getObject(InputStream.class)) { + config = JaxbUtils.unmarshal(TrapdConfiguration.class, inputStream); + } catch (Exception e) { + LOG.warn("Failed to parse uploaded trapd configuration.", e); + return Response.status(Status.BAD_REQUEST).entity("Invalid trapd XML configuration.").build(); + } + + try { + m_trapdConfigDao.updateConfig(config); + return Response.ok(m_trapdConfigDao.getConfig()).build(); + } catch (ValidationException e) { + LOG.warn("Uploaded trapd configuration failed schema validation.", e); + return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Failed to persist uploaded trapd configuration.", e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to persist trapd configuration.").build(); + } + } + + @Override + public Response getTrapdConfiguration(final SecurityContext securityContext) { + try { + TrapdConfiguration config = m_trapdConfigDao.getConfig(); + if (config == null) { + return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); + } + return Response.ok(config).build(); + } catch (Exception e) { + LOG.error("Failed to retrieve trapd configuration.", e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to retrieve trapd configuration.").build(); + } + } + + @Override + public Response updateTrapdConfiguration(TrapdConfigPayload config, SecurityContext securityContext) { + if (config == null) { + return Response.status(Status.BAD_REQUEST).entity("Missing trapd configuration in request body.").build(); + } + + final TrapdConfiguration updatedConfig; + try { + updatedConfig = mergeTrapdConfiguration(config); + } catch (Exception e) { + LOG.warn("Failed to map trapd update payload.", e); + return Response.status(Status.BAD_REQUEST).entity("Invalid trapd configuration payload.").build(); + } + + try { + m_trapdConfigDao.updateConfig(updatedConfig); + return Response.ok(m_trapdConfigDao.getConfig()).build(); + } catch (ValidationException e) { + LOG.warn("Provided trapd configuration failed schema validation.", e); + return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Failed to persist provided trapd configuration.", e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to persist trapd configuration.").build(); + } + } + + private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigPayload payload) { + TrapdConfiguration config = m_trapdConfigDao.getConfig(); + if (config == null) { + config = new TrapdConfiguration(); + } + + if (payload.getSnmpTrapAddress() != null) { + config.setSnmpTrapAddress(payload.getSnmpTrapAddress()); + } + if (payload.getSnmpTrapPort() != null) { + config.setSnmpTrapPort(payload.getSnmpTrapPort()); + } + if (payload.getNewSuspectOnTrap() != null) { + config.setNewSuspectOnTrap(payload.getNewSuspectOnTrap()); + } + if (payload.getIncludeRawMessage() != null) { + config.setIncludeRawMessage(payload.getIncludeRawMessage()); + } + if (payload.getThreads() != null) { + config.setThreads(payload.getThreads()); + } + if (payload.getQueueSize() != null) { + config.setQueueSize(payload.getQueueSize()); + } + if (payload.getBatchSize() != null) { + config.setBatchSize(payload.getBatchSize()); + } + if (payload.getBatchInterval() != null) { + config.setBatchInterval(payload.getBatchInterval()); + } + if (payload.getUseAddressFromVarbind() != null) { + config.setUseAddressFromVarbind(payload.getUseAddressFromVarbind()); + } + if (payload.getSnmpv3Users() != null) { + config.setSnmpv3User(payload.getSnmpv3Users()); + } + + // Prevent generated helper flags from being persisted as schema properties. +// config.deleteNewSuspectOnTrap(); +// config.deleteSnmpTrapPort(); + return config; + } + +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java new file mode 100644 index 000000000000..11d6ca26aa83 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java @@ -0,0 +1,90 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.web.rest.v2.api; + +import javax.ws.rs.*; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.apache.cxf.jaxrs.ext.multipart.Multipart; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.opennms.web.rest.v2.model.TrapdConfigPayload; + +@Path("trapd") +@Tag(name = "Trapd", description = "Trapd API V2") +public interface TrapdRestApi { + + @POST + @Path("upload") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Upload trapd configuration", + description = "Upload trapd-configuration XML and persist it to DB.", + operationId = "uploadTrapdConfiguration" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "Configuration updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid trapd XML or missing upload field"), + @ApiResponse(responseCode = "500", description = "Failed to persist trapd configuration") + }) + Response uploadTrapdConfiguration(@Multipart("upload") Attachment attachment, @Context SecurityContext securityContext); + + @GET + @Path("get-config") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get trapd configuration", + description = "Retrieve the current trapd configuration.", + operationId = "getTrapdConfiguration" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Configuration retrieved successfully"), + @ApiResponse(responseCode = "404", description = "Configuration not found"), + @ApiResponse(responseCode = "500", description = "Failed to retrieve trapd configuration") + }) + Response getTrapdConfiguration(@Context SecurityContext securityContext); + + @POST + @Path("update-config") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update trapd configuration", + description = "Update trapd configuration with provided JSON payload.", + operationId = "updateTrapdConfiguration" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "Configuration updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid configuration payload"), + @ApiResponse(responseCode = "500", description = "Failed to update trapd configuration") + }) + Response updateTrapdConfiguration(TrapdConfigPayload payload, @Context SecurityContext securityContext); +} + diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java new file mode 100644 index 000000000000..c4dad14b7d18 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java @@ -0,0 +1,98 @@ +package org.opennms.web.rest.v2.model; + +import java.util.List; + +import org.opennms.netmgt.config.trapd.Snmpv3User; + +public class TrapdConfigPayload { + private String snmpTrapAddress; + private Integer snmpTrapPort; + private Boolean newSuspectOnTrap; + private Boolean includeRawMessage; + private Integer threads; + private Integer queueSize; + private Integer batchSize; + private Integer batchInterval; + private Boolean useAddressFromVarbind; + private List snmpv3Users; + + public String getSnmpTrapAddress() { + return snmpTrapAddress; + } + + public void setSnmpTrapAddress(final String snmpTrapAddress) { + this.snmpTrapAddress = snmpTrapAddress; + } + + public Integer getSnmpTrapPort() { + return snmpTrapPort; + } + + public void setSnmpTrapPort(final Integer snmpTrapPort) { + this.snmpTrapPort = snmpTrapPort; + } + + public Boolean getNewSuspectOnTrap() { + return newSuspectOnTrap; + } + + public void setNewSuspectOnTrap(final Boolean newSuspectOnTrap) { + this.newSuspectOnTrap = newSuspectOnTrap; + } + + public Boolean getIncludeRawMessage() { + return includeRawMessage; + } + + public void setIncludeRawMessage(final Boolean includeRawMessage) { + this.includeRawMessage = includeRawMessage; + } + + public Integer getThreads() { + return threads; + } + + public void setThreads(final Integer threads) { + this.threads = threads; + } + + public Integer getQueueSize() { + return queueSize; + } + + public void setQueueSize(final Integer queueSize) { + this.queueSize = queueSize; + } + + public Integer getBatchSize() { + return batchSize; + } + + public void setBatchSize(final Integer batchSize) { + this.batchSize = batchSize; + } + + public Integer getBatchInterval() { + return batchInterval; + } + + public void setBatchInterval(final Integer batchInterval) { + this.batchInterval = batchInterval; + } + + public Boolean getUseAddressFromVarbind() { + return useAddressFromVarbind; + } + + public void setUseAddressFromVarbind(final Boolean useAddressFromVarbind) { + this.useAddressFromVarbind = useAddressFromVarbind; + } + + public List getSnmpv3Users() { + return snmpv3Users; + } + + public void setSnmpv3Users(final List snmpv3Users) { + this.snmpv3Users = snmpv3Users; + } +} diff --git a/opennms-webapp-rest/src/main/webapp/WEB-INF/applicationContext-cxf-rest-v2.xml b/opennms-webapp-rest/src/main/webapp/WEB-INF/applicationContext-cxf-rest-v2.xml index 1275108b1a66..17e22ab488d0 100644 --- a/opennms-webapp-rest/src/main/webapp/WEB-INF/applicationContext-cxf-rest-v2.xml +++ b/opennms-webapp-rest/src/main/webapp/WEB-INF/applicationContext-cxf-rest-v2.xml @@ -6,18 +6,21 @@ xmlns:util="http://www.springframework.org/schema/util" xmlns:cxf="http://cxf.apache.org/core" xmlns:jaxrs="http://cxf.apache.org/jaxrs" + xmlns:onmsgi="http://xmlns.opennms.org/xsd/spring/onms-osgi" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd - http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd"> + http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd + http://xmlns.opennms.org/xsd/spring/onms-osgi http://xmlns.opennms.org/xsd/spring/onms-osgi.xsd"> + diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java new file mode 100644 index 000000000000..07d2815f9bcd --- /dev/null +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java @@ -0,0 +1,119 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.web.rest.v2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; + +import org.apache.cxf.jaxrs.ext.multipart.Attachment; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.opennms.core.test.OpenNMSJUnit4ClassRunner; +import org.opennms.core.test.db.annotations.JUnitTemporaryDatabase; +import org.opennms.netmgt.dao.api.TrapdConfigDao; +import org.opennms.test.JUnitConfigurationEnvironment; +import org.opennms.web.rest.v2.api.TrapdRestApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; + +@RunWith(OpenNMSJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(locations = { + "classpath:/META-INF/opennms/applicationContext-soa.xml", + "classpath:/META-INF/opennms/applicationContext-commonConfigs.xml", + "classpath:/META-INF/opennms/applicationContext-dao.xml", + "classpath*:/META-INF/opennms/component-dao.xml", + "classpath:/META-INF/opennms/mockEventIpcManager.xml", + "classpath:/applicationContext-rest-test.xml" +}) +@JUnitConfigurationEnvironment(systemProperties = "org.opennms.timeseries.strategy=integration") +@JUnitTemporaryDatabase +public class TrapdRestServiceIT { + + private SecurityContext securityContext; + + @Autowired + private TrapdRestApi m_trapdRestApi; + + // @Autowired + // private TrapdConfigDao m_trapdConfigDao; + + // @Before + // public void setUp() { + // Principal principal = mock(Principal.class); + // when(principal.getName()).thenReturn("integration-user"); + // securityContext = mock(SecurityContext.class); + // when(securityContext.getUserPrincipal()).thenReturn(principal); + // } + + // @Test + // public void testUploadTrapdConfig() { + // final Attachment attachment = mock(Attachment.class); + // when(attachment.getObject(InputStream.class)).thenReturn(new ByteArrayInputStream(validTrapdConfigXml().getBytes(StandardCharsets.UTF_8))); + + // try (Response response = m_trapdRestApi.uploadTrapdConfiguration(attachment, securityContext)) { + // assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); + // } + // assertEquals(10163, m_trapdConfigDao.getConfig().getSnmpTrapPort()); + // } + + // @Test + // public void testUploadTrapdConfigWithInvalidXml() { + // final Attachment attachment = mock(Attachment.class); + // when(attachment.getObject(InputStream.class)).thenReturn(new ByteArrayInputStream(""; + // } +} + + diff --git a/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml b/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml index fefa36f61f93..4632db98874d 100644 --- a/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml +++ b/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml @@ -11,6 +11,8 @@ + + From 1a4d4c2766662b4d25258e97ff53794f550aba1d Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 17 Mar 2026 19:28:57 +0500 Subject: [PATCH 02/41] changes for dto --- .../opennms/web/rest/v2/TrapdRestService.java | 19 +++++--------- .../opennms/web/rest/v2/api/TrapdRestApi.java | 4 +-- ...ConfigPayload.java => TrapdConfigDto.java} | 25 +++++++++++-------- 3 files changed, 22 insertions(+), 26 deletions(-) rename opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/{TrapdConfigPayload.java => TrapdConfigDto.java} (73%) diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java index c990b0549fb6..bccf6523841e 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java @@ -33,7 +33,7 @@ import org.opennms.netmgt.config.trapd.TrapdConfiguration; import org.opennms.netmgt.dao.api.TrapdConfigDao; import org.opennms.web.rest.v2.api.TrapdRestApi; -import org.opennms.web.rest.v2.model.TrapdConfigPayload; +import org.opennms.web.rest.v2.model.TrapdConfigDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -63,7 +63,7 @@ public Response uploadTrapdConfiguration(final Attachment attachment, final Secu try { m_trapdConfigDao.updateConfig(config); - return Response.ok(m_trapdConfigDao.getConfig()).build(); + return Response.ok(new TrapdConfigDto().toDto(config)).build(); } catch (ValidationException e) { LOG.warn("Uploaded trapd configuration failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -80,7 +80,7 @@ public Response getTrapdConfiguration(final SecurityContext securityContext) { if (config == null) { return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); } - return Response.ok(config).build(); + return Response.ok(new TrapdConfigDto().toDto(config)).build(); } catch (Exception e) { LOG.error("Failed to retrieve trapd configuration.", e); return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to retrieve trapd configuration.").build(); @@ -88,7 +88,7 @@ public Response getTrapdConfiguration(final SecurityContext securityContext) { } @Override - public Response updateTrapdConfiguration(TrapdConfigPayload config, SecurityContext securityContext) { + public Response updateTrapdConfiguration(TrapdConfigDto config, SecurityContext securityContext) { if (config == null) { return Response.status(Status.BAD_REQUEST).entity("Missing trapd configuration in request body.").build(); } @@ -103,7 +103,7 @@ public Response updateTrapdConfiguration(TrapdConfigPayload config, SecurityCont try { m_trapdConfigDao.updateConfig(updatedConfig); - return Response.ok(m_trapdConfigDao.getConfig()).build(); + return Response.ok(new TrapdConfigDto().toDto(m_trapdConfigDao.getConfig())).build(); } catch (ValidationException e) { LOG.warn("Provided trapd configuration failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -113,7 +113,7 @@ public Response updateTrapdConfiguration(TrapdConfigPayload config, SecurityCont } } - private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigPayload payload) { + private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigDto payload) { TrapdConfiguration config = m_trapdConfigDao.getConfig(); if (config == null) { config = new TrapdConfiguration(); @@ -146,13 +146,6 @@ private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigPayload payl if (payload.getUseAddressFromVarbind() != null) { config.setUseAddressFromVarbind(payload.getUseAddressFromVarbind()); } - if (payload.getSnmpv3Users() != null) { - config.setSnmpv3User(payload.getSnmpv3Users()); - } - - // Prevent generated helper flags from being persisted as schema properties. -// config.deleteNewSuspectOnTrap(); -// config.deleteSnmpTrapPort(); return config; } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java index 11d6ca26aa83..00f2c09bc8e4 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java @@ -34,7 +34,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import org.opennms.web.rest.v2.model.TrapdConfigPayload; +import org.opennms.web.rest.v2.model.TrapdConfigDto; @Path("trapd") @Tag(name = "Trapd", description = "Trapd API V2") @@ -85,6 +85,6 @@ public interface TrapdRestApi { @ApiResponse(responseCode = "400", description = "Invalid configuration payload"), @ApiResponse(responseCode = "500", description = "Failed to update trapd configuration") }) - Response updateTrapdConfiguration(TrapdConfigPayload payload, @Context SecurityContext securityContext); + Response updateTrapdConfiguration(TrapdConfigDto payload, @Context SecurityContext securityContext); } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java similarity index 73% rename from opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java rename to opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java index c4dad14b7d18..afaacc8163db 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigPayload.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java @@ -1,10 +1,8 @@ package org.opennms.web.rest.v2.model; -import java.util.List; +import org.opennms.netmgt.config.trapd.TrapdConfiguration; -import org.opennms.netmgt.config.trapd.Snmpv3User; - -public class TrapdConfigPayload { +public class TrapdConfigDto { private String snmpTrapAddress; private Integer snmpTrapPort; private Boolean newSuspectOnTrap; @@ -14,7 +12,6 @@ public class TrapdConfigPayload { private Integer batchSize; private Integer batchInterval; private Boolean useAddressFromVarbind; - private List snmpv3Users; public String getSnmpTrapAddress() { return snmpTrapAddress; @@ -88,11 +85,17 @@ public void setUseAddressFromVarbind(final Boolean useAddressFromVarbind) { this.useAddressFromVarbind = useAddressFromVarbind; } - public List getSnmpv3Users() { - return snmpv3Users; - } - - public void setSnmpv3Users(final List snmpv3Users) { - this.snmpv3Users = snmpv3Users; + public TrapdConfigDto toDto(final TrapdConfiguration config) { + TrapdConfigDto dto = new TrapdConfigDto(); + dto.setSnmpTrapAddress(config.getSnmpTrapAddress()); + dto.setSnmpTrapPort(config.getSnmpTrapPort()); + dto.setNewSuspectOnTrap(config.getNewSuspectOnTrap()); + dto.setIncludeRawMessage(config.isIncludeRawMessage()); + dto.setThreads(config.getThreads()); + dto.setQueueSize(config.getQueueSize()); + dto.setBatchSize(config.getBatchSize()); + dto.setBatchInterval(config.getBatchInterval()); + dto.setUseAddressFromVarbind(config.shouldUseAddressFromVarbind()); + return dto; } } From 2a4c5ac123692aed676fb21108fe7b05b9d00482 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 17 Mar 2026 19:49:54 +0500 Subject: [PATCH 03/41] test cases --- .../web/rest/v2/TrapdRestServiceIT.java | 218 +++++++++++++----- .../applicationContext-rest-test.xml | 2 +- 2 files changed, 164 insertions(+), 56 deletions(-) diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java index 07d2815f9bcd..5fbafd00aff1 100644 --- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java @@ -22,28 +22,30 @@ package org.opennms.web.rest.v2; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; -import java.security.Principal; import javax.ws.rs.core.Response; -import javax.ws.rs.core.SecurityContext; import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.opennms.core.test.OpenNMSJUnit4ClassRunner; import org.opennms.core.test.db.annotations.JUnitTemporaryDatabase; +import org.opennms.features.config.exception.ValidationException; +import org.opennms.netmgt.config.trapd.TrapdConfiguration; import org.opennms.netmgt.dao.api.TrapdConfigDao; import org.opennms.test.JUnitConfigurationEnvironment; -import org.opennms.web.rest.v2.api.TrapdRestApi; +import org.opennms.web.rest.v2.model.TrapdConfigDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.web.WebAppConfiguration; @@ -62,58 +64,164 @@ @JUnitTemporaryDatabase public class TrapdRestServiceIT { - private SecurityContext securityContext; + @Autowired + private TrapdRestService m_trapdRestService; @Autowired - private TrapdRestApi m_trapdRestApi; - - // @Autowired - // private TrapdConfigDao m_trapdConfigDao; - - // @Before - // public void setUp() { - // Principal principal = mock(Principal.class); - // when(principal.getName()).thenReturn("integration-user"); - // securityContext = mock(SecurityContext.class); - // when(securityContext.getUserPrincipal()).thenReturn(principal); - // } - - // @Test - // public void testUploadTrapdConfig() { - // final Attachment attachment = mock(Attachment.class); - // when(attachment.getObject(InputStream.class)).thenReturn(new ByteArrayInputStream(validTrapdConfigXml().getBytes(StandardCharsets.UTF_8))); - - // try (Response response = m_trapdRestApi.uploadTrapdConfiguration(attachment, securityContext)) { - // assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); - // } - // assertEquals(10163, m_trapdConfigDao.getConfig().getSnmpTrapPort()); - // } - - // @Test - // public void testUploadTrapdConfigWithInvalidXml() { - // final Attachment attachment = mock(Attachment.class); - // when(attachment.getObject(InputStream.class)).thenReturn(new ByteArrayInputStream(""; - // } + private TrapdConfigDao m_trapdConfigDao; + + @Before + public void setUp() throws Exception { + m_trapdRestService = new TrapdRestService(); + m_trapdConfigDao = mock(TrapdConfigDao.class); + setField(m_trapdRestService, "m_trapdConfigDao", m_trapdConfigDao); + } + + @Test + public void uploadShouldReturnBadRequestWhenAttachmentMissing() { + try (Response response = m_trapdRestService.uploadTrapdConfiguration(null, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Missing uploaded file field 'upload'.", response.getEntity()); + } + } + + @Test + public void uploadShouldReturnBadRequestWhenXmlIsInvalid() { + Attachment attachment = mock(Attachment.class); + when(attachment.getObject(InputStream.class)).thenReturn( + new ByteArrayInputStream(" captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + assertEquals(10163, captor.getValue().getSnmpTrapPort()); + } + + @Test + public void uploadShouldReturnBadRequestWhenValidationFails() { + Attachment attachment = mock(Attachment.class); + when(attachment.getObject(InputStream.class)).thenReturn( + new ByteArrayInputStream(validTrapdConfigXml().getBytes(StandardCharsets.UTF_8)) + ); + whenValidationFailsOnUpdate("schema error"); + + try (Response response = m_trapdRestService.uploadTrapdConfiguration(attachment, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("schema error", response.getEntity()); + } + } + + @Test + public void getShouldReturnNotFoundWhenNoConfigurationExists() { + when(m_trapdConfigDao.getConfig()).thenReturn(null); + + try (Response response = m_trapdRestService.getTrapdConfiguration(null)) { + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + assertEquals("Trapd configuration not found.", response.getEntity()); + } + } + + @Test + public void updateShouldReturnBadRequestWhenPayloadMissing() { + try (Response response = m_trapdRestService.updateTrapdConfiguration(null, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Missing trapd configuration in request body.", response.getEntity()); + } + } + + @Test + public void updateShouldMergePayloadAndPersist() { + TrapdConfiguration existing = new TrapdConfiguration(); + existing.setSnmpTrapAddress("127.0.0.1"); + existing.setSnmpTrapPort(1162); + existing.setThreads(4); + existing.setQueueSize(1000); + when(m_trapdConfigDao.getConfig()).thenReturn(existing, existing); + + TrapdConfigDto payload = new TrapdConfigDto(); + payload.setSnmpTrapPort(10164); + payload.setIncludeRawMessage(Boolean.TRUE); + + try (Response response = m_trapdRestService.updateTrapdConfiguration(payload, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + TrapdConfigDto dto = (TrapdConfigDto) response.getEntity(); + assertEquals(Integer.valueOf(10164), dto.getSnmpTrapPort()); + assertEquals(Integer.valueOf(4), dto.getThreads()); + assertEquals("127.0.0.1", dto.getSnmpTrapAddress()); + assertEquals(Boolean.TRUE, dto.getIncludeRawMessage()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + TrapdConfiguration persisted = captor.getValue(); + assertEquals(10164, persisted.getSnmpTrapPort()); + assertEquals(4, persisted.getThreads()); + } + + @Test + public void updateShouldReturnBadRequestWhenValidationFails() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + whenValidationFailsOnUpdate("validation failed"); + + TrapdConfigDto payload = new TrapdConfigDto(); + payload.setSnmpTrapPort(10164); + + try (Response response = m_trapdRestService.updateTrapdConfiguration(payload, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("validation failed", response.getEntity()); + } + } + + @Test + public void updateShouldReturnServerErrorWhenPersistenceThrows() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + + TrapdConfigDto payload = new TrapdConfigDto(); + payload.setSnmpTrapPort(10164); + + try (Response response = m_trapdRestService.updateTrapdConfiguration(payload, null)) { + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("Failed to persist trapd configuration.", response.getEntity()); + } + } + + private void whenValidationFailsOnUpdate(final String message) { + org.mockito.Mockito.doThrow(new ValidationException(message)).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + private static void setField(final Object target, final String fieldName, final Object value) throws Exception { + Field field = target.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(target, value); + } + + private static String validTrapdConfigXml() { + return ""; + } } - diff --git a/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml b/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml index 4632db98874d..90419457b7ad 100644 --- a/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml +++ b/opennms-webapp-rest/src/test/resources/applicationContext-rest-test.xml @@ -12,7 +12,7 @@ - + From dbfa3e4c5f2b1e901464a975007bcc913160a3ee Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 17 Mar 2026 21:38:25 +0500 Subject: [PATCH 04/41] new rest endpoints or snmp v3 users --- .../opennms/web/rest/v2/TrapdRestService.java | 189 ++++++ .../opennms/web/rest/v2/api/TrapdRestApi.java | 52 +- .../web/rest/v2/model/Snmpv3UserDto.java | 106 ++++ .../web/rest/v2/model/TrapdConfigDto.java | 30 + .../web/rest/v2/TrapdRestServiceIT.java | 558 ++++++++++++++++++ 5 files changed, 934 insertions(+), 1 deletion(-) create mode 100644 opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java index bccf6523841e..5c2262c66b08 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java @@ -22,6 +22,9 @@ package org.opennms.web.rest.v2; import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -30,9 +33,11 @@ import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.opennms.core.xml.JaxbUtils; import org.opennms.features.config.exception.ValidationException; +import org.opennms.netmgt.config.trapd.Snmpv3User; import org.opennms.netmgt.config.trapd.TrapdConfiguration; import org.opennms.netmgt.dao.api.TrapdConfigDao; import org.opennms.web.rest.v2.api.TrapdRestApi; +import org.opennms.web.rest.v2.model.Snmpv3UserDto; import org.opennms.web.rest.v2.model.TrapdConfigDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +48,8 @@ public class TrapdRestService implements TrapdRestApi { private static final Logger LOG = LoggerFactory.getLogger(TrapdRestService.class); + private static final Set AUTH_PROTOCOLS = new HashSet<>(Arrays.asList("MD5", "SHA", "SHA-224", "SHA-256", "SHA-512")); + private static final Set PRIVACY_PROTOCOLS = new HashSet<>(Arrays.asList("DES", "AES", "AES192", "AES256")); @Autowired private TrapdConfigDao m_trapdConfigDao; @@ -113,6 +120,128 @@ public Response updateTrapdConfiguration(TrapdConfigDto config, SecurityContext } } + @Override + public Response saveTrapdUser(Snmpv3UserDto user, SecurityContext securityContext) { + if (user == null) { + return Response.status(Status.BAD_REQUEST).entity("Missing SNMPv3 user in request body.").build(); + } + + final Snmpv3User snmpv3User; + try { + snmpv3User = toSnmpv3User(user); + } catch (Exception e) { + LOG.warn("Failed to map SNMPv3 user payload.", e); + return Response.status(Status.BAD_REQUEST).entity("Invalid SNMPv3 user payload.").build(); + } + + try { + TrapdConfiguration config = m_trapdConfigDao.getConfig(); + if (config == null) { + config = new TrapdConfiguration(); + } + + final String validationMessage = validateSnmpv3UserPayload(user); + if (validationMessage != null) { + return Response.status(Status.BAD_REQUEST).entity(validationMessage).build(); + } + + // Ensure required attributes are present when persisting a newly-created config. + if (!config.hasSnmpTrapPort()) { + config.setSnmpTrapPort(162); + } + if (!config.hasNewSuspectOnTrap()) { + config.setNewSuspectOnTrap(false); + } + + config.addSnmpv3User(snmpv3User); + m_trapdConfigDao.updateConfig(config); + return Response.ok(new Snmpv3UserDto().toDto(snmpv3User)).build(); + } catch (ValidationException e) { + LOG.warn("Provided SNMPv3 user failed schema validation.", e); + return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Failed to persist provided SNMPv3 user.", e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to persist trapd user.").build(); + } + } + + @Override + public Response updateTrapdUser(Integer index, Snmpv3UserDto user, SecurityContext securityContext) { + if (index == null || index < 0) { + return Response.status(Status.BAD_REQUEST).entity("Valid user index is required.").build(); + } + + if (user == null) { + return Response.status(Status.BAD_REQUEST).entity("Missing SNMPv3 user in request body.").build(); + } + + final Snmpv3User snmpv3User; + try { + snmpv3User = toSnmpv3User(user); + } catch (Exception e) { + LOG.warn("Failed to map SNMPv3 user payload.", e); + return Response.status(Status.BAD_REQUEST).entity("Invalid SNMPv3 user payload.").build(); + } + + try { + final TrapdConfiguration config = m_trapdConfigDao.getConfig(); + if (config == null) { + return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); + } + + if (index >= config.getSnmpv3UserCount()) { + return Response.status(Status.BAD_REQUEST) + .entity("Index " + index + " is out of range. There are " + config.getSnmpv3UserCount() + " user(s) configured.") + .build(); + } + + final String validationMessage = validateSnmpv3UserPayload(user); + if (validationMessage != null) { + return Response.status(Status.BAD_REQUEST).entity(validationMessage).build(); + } + + config.setSnmpv3User(index, snmpv3User); + m_trapdConfigDao.updateConfig(config); + return Response.ok(new Snmpv3UserDto().toDto(snmpv3User)).build(); + } catch (ValidationException e) { + LOG.warn("Updating SNMPv3 user failed schema validation.", e); + return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Failed to update SNMPv3 user at index {}.", index, e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to update trapd user.").build(); + } + } + + @Override + public Response deleteTrapdUser(Integer index, SecurityContext securityContext) { + if (index == null || index < 0) { + return Response.status(Status.BAD_REQUEST).entity("Valid user index is required.").build(); + } + + try { + final TrapdConfiguration config = m_trapdConfigDao.getConfig(); + if (config == null) { + return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); + } + + if (index >= config.getSnmpv3UserCount()) { + return Response.status(Status.BAD_REQUEST) + .entity("Index " + index + " is out of range. There are " + config.getSnmpv3UserCount() + " user(s) configured.") + .build(); + } + + final Snmpv3User removed = config.removeSnmpv3UserAt(index); + m_trapdConfigDao.updateConfig(config); + return Response.ok(new Snmpv3UserDto().toDto(removed)).build(); + } catch (ValidationException e) { + LOG.warn("Removing SNMPv3 user failed schema validation.", e); + return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); + } catch (Exception e) { + LOG.error("Failed to delete SNMPv3 user at index {}.", index, e); + return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to delete trapd user.").build(); + } + } + private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigDto payload) { TrapdConfiguration config = m_trapdConfigDao.getConfig(); if (config == null) { @@ -149,4 +278,64 @@ private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfigDto payload) return config; } + private Snmpv3User toSnmpv3User(final Snmpv3UserDto userDto) { + final Snmpv3User user = new Snmpv3User(); + user.setEngineId(userDto.getEngineId()); + user.setSecurityName(userDto.getSecurityName()); + user.setSecurityLevel(userDto.getSecurityLevel()); + user.setAuthProtocol(userDto.getAuthProtocol()); + user.setAuthPassphrase(userDto.getAuthPassphrase()); + user.setPrivacyProtocol(userDto.getPrivacyProtocol()); + user.setPrivacyPassphrase(userDto.getPrivacyPassphrase()); + return user; + } + + private String validateSnmpv3UserPayload(final Snmpv3UserDto user) { + if (isBlank(user.getSecurityName())) { + return "securityName is required."; + } + + final Integer securityLevel = user.getSecurityLevel(); + if (securityLevel != null && (securityLevel < 1 || securityLevel > 3)) { + return "securityLevel must be between 1 and 3."; + } + + if (!isBlank(user.getAuthProtocol()) && !AUTH_PROTOCOLS.contains(user.getAuthProtocol())) { + return "Unsupported authProtocol."; + } + if (!isBlank(user.getPrivacyProtocol()) && !PRIVACY_PROTOCOLS.contains(user.getPrivacyProtocol())) { + return "Unsupported privacyProtocol."; + } + + final boolean hasAuthProtocol = !isBlank(user.getAuthProtocol()); + final boolean hasAuthPassphrase = !isBlank(user.getAuthPassphrase()); + final boolean hasPrivacyProtocol = !isBlank(user.getPrivacyProtocol()); + final boolean hasPrivacyPassphrase = !isBlank(user.getPrivacyPassphrase()); + + if (hasAuthProtocol != hasAuthPassphrase) { + return "authProtocol and authPassphrase must be provided together."; + } + if (hasPrivacyProtocol != hasPrivacyPassphrase) { + return "privacyProtocol and privacyPassphrase must be provided together."; + } + + if (securityLevel != null) { + if (securityLevel == 1 && (hasAuthProtocol || hasPrivacyProtocol)) { + return "securityLevel 1 does not allow auth or privacy credentials."; + } + if (securityLevel == 2 && (!hasAuthProtocol || hasPrivacyProtocol)) { + return "securityLevel 2 requires auth credentials and does not allow privacy credentials."; + } + if (securityLevel == 3 && (!hasAuthProtocol || !hasPrivacyProtocol)) { + return "securityLevel 3 requires both auth and privacy credentials."; + } + } + + return null; + } + + private boolean isBlank(final String value) { + return value == null || value.trim().isEmpty(); + } + } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java index 00f2c09bc8e4..420c254f0ce6 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java @@ -26,6 +26,7 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.QueryParam; import org.apache.cxf.jaxrs.ext.multipart.Attachment; import org.apache.cxf.jaxrs.ext.multipart.Multipart; @@ -34,6 +35,7 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; +import org.opennms.web.rest.v2.model.Snmpv3UserDto; import org.opennms.web.rest.v2.model.TrapdConfigDto; @Path("trapd") @@ -71,7 +73,7 @@ public interface TrapdRestApi { }) Response getTrapdConfiguration(@Context SecurityContext securityContext); - @POST + @PUT @Path("update-config") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @@ -86,5 +88,53 @@ public interface TrapdRestApi { @ApiResponse(responseCode = "500", description = "Failed to update trapd configuration") }) Response updateTrapdConfiguration(TrapdConfigDto payload, @Context SecurityContext securityContext); + + @POST + @Path("save-user") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Save SNMPv3 user", + description = "Save SNMPv3 user configuration with provided JSON payload.", + operationId = "saveTrapdUser" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User saved successfully"), + @ApiResponse(responseCode = "400", description = "Invalid user payload"), + @ApiResponse(responseCode = "500", description = "Failed to save user") + }) + Response saveTrapdUser(Snmpv3UserDto user, @Context SecurityContext securityContext); + + @PUT + @Path("update-user") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update SNMPv3 user", + description = "Update SNMPv3 user configuration with provided JSON payload.", + operationId = "updateTrapdUser" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User updated successfully"), + @ApiResponse(responseCode = "400", description = "Invalid user payload"), + @ApiResponse(responseCode = "500", description = "Failed to update user") + }) + Response updateTrapdUser(@QueryParam("index") Integer index, Snmpv3UserDto user, @Context SecurityContext securityContext); + + @DELETE + @Path("delete-user") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Delete SNMPv3 user", + description = "Delete SNMPv3 user configuration with provided index.", + operationId = "deleteTrapdUser" + ) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "User deleted successfully"), + @ApiResponse(responseCode = "400", description = "Invalid user index"), + @ApiResponse(responseCode = "500", description = "Failed to delete user") + }) + Response deleteTrapdUser(@QueryParam("index") Integer index, @Context SecurityContext securityContext); } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java new file mode 100644 index 000000000000..5dc6fbd968ad --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java @@ -0,0 +1,106 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ +package org.opennms.web.rest.v2.model; + +import org.opennms.netmgt.config.trapd.Snmpv3User; + +public class Snmpv3UserDto { + private String engineId; + private String securityName; + private Integer securityLevel; + private String authProtocol; + private String authPassphrase; + private String privacyProtocol; + private String privacyPassphrase; + + + public String getEngineId() { + return engineId; + } + + public void setEngineId(String engineId) { + this.engineId = engineId; + } + + public String getSecurityName() { + return securityName; + } + + public void setSecurityName(String securityName) { + this.securityName = securityName; + } + + public Integer getSecurityLevel() { + return securityLevel; + } + + public void setSecurityLevel(Integer securityLevel) { + this.securityLevel = securityLevel; + } + + public String getAuthProtocol() { + return authProtocol; + } + + public void setAuthProtocol(String authProtocol) { + this.authProtocol = authProtocol; + } + + public String getAuthPassphrase() { + return authPassphrase; + } + + public void setAuthPassphrase(String authPassphrase) { + this.authPassphrase = authPassphrase; + } + + public String getPrivacyProtocol() { + return privacyProtocol; + } + + public void setPrivacyProtocol(String privacyProtocol) { + this.privacyProtocol = privacyProtocol; + } + + public String getPrivacyPassphrase() { + return privacyPassphrase; + } + + public void setPrivacyPassphrase(String privacyPassphrase) { + this.privacyPassphrase = privacyPassphrase; + } + + public Snmpv3UserDto toDto(Snmpv3User user) { + if (user == null) { + return null; + } + Snmpv3UserDto dto = new Snmpv3UserDto(); + dto.setEngineId(user.getEngineId()); + dto.setSecurityName(user.getSecurityName()); + dto.setSecurityLevel(user.getSecurityLevel()); + dto.setAuthProtocol(user.getAuthProtocol()); + dto.setAuthPassphrase(user.getAuthPassphrase()); + dto.setPrivacyProtocol(user.getPrivacyProtocol()); + dto.setPrivacyPassphrase(user.getPrivacyPassphrase()); + return dto; + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java index afaacc8163db..3f8f432861fd 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java @@ -1,7 +1,31 @@ +/* + * Licensed to The OpenNMS Group, Inc (TOG) under one or more + * contributor license agreements. See the LICENSE.md file + * distributed with this work for additional information + * regarding copyright ownership. + * + * TOG licenses this file to You under the GNU Affero General + * Public License Version 3 (the "License") or (at your option) + * any later version. You may not use this file except in + * compliance with the License. You may obtain a copy of the + * License at: + * + * https://www.gnu.org/licenses/agpl-3.0.txt + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the + * License. + */ package org.opennms.web.rest.v2.model; +import org.opennms.netmgt.config.trapd.Snmpv3User; import org.opennms.netmgt.config.trapd.TrapdConfiguration; +import java.util.List; + public class TrapdConfigDto { private String snmpTrapAddress; private Integer snmpTrapPort; @@ -12,6 +36,7 @@ public class TrapdConfigDto { private Integer batchSize; private Integer batchInterval; private Boolean useAddressFromVarbind; + private List snmpv3User; public String getSnmpTrapAddress() { return snmpTrapAddress; @@ -85,6 +110,10 @@ public void setUseAddressFromVarbind(final Boolean useAddressFromVarbind) { this.useAddressFromVarbind = useAddressFromVarbind; } + public List getSnmpv3User() { return snmpv3User; } + + public void setSnmpv3User(List snmpv3Users) { this.snmpv3User = snmpv3Users; } + public TrapdConfigDto toDto(final TrapdConfiguration config) { TrapdConfigDto dto = new TrapdConfigDto(); dto.setSnmpTrapAddress(config.getSnmpTrapAddress()); @@ -96,6 +125,7 @@ public TrapdConfigDto toDto(final TrapdConfiguration config) { dto.setBatchSize(config.getBatchSize()); dto.setBatchInterval(config.getBatchInterval()); dto.setUseAddressFromVarbind(config.shouldUseAddressFromVarbind()); + dto.setSnmpv3User(List.of(config.getSnmpv3User())); return dto; } } diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java index 5fbafd00aff1..3855895f853e 100644 --- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,6 +46,8 @@ import org.opennms.netmgt.config.trapd.TrapdConfiguration; import org.opennms.netmgt.dao.api.TrapdConfigDao; import org.opennms.test.JUnitConfigurationEnvironment; +import org.opennms.netmgt.config.trapd.Snmpv3User; +import org.opennms.web.rest.v2.model.Snmpv3UserDto; import org.opennms.web.rest.v2.model.TrapdConfigDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; @@ -132,6 +135,37 @@ public void uploadShouldReturnBadRequestWhenValidationFails() { } } + @Test + public void uploadShouldReturnServerErrorWhenPersistenceFails() { + Attachment attachment = mock(Attachment.class); + when(attachment.getObject(InputStream.class)).thenReturn( + new ByteArrayInputStream(validTrapdConfigXml().getBytes(StandardCharsets.UTF_8)) + ); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + + try (Response response = m_trapdRestService.uploadTrapdConfiguration(attachment, null)) { + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("Failed to persist trapd configuration.", response.getEntity()); + } + } + + @Test + public void getShouldReturnOkWithConfigWhenExists() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setSnmpTrapAddress("127.0.0.1"); + config.setNewSuspectOnTrap(false); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + try (Response response = m_trapdRestService.getTrapdConfiguration(null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof TrapdConfigDto); + TrapdConfigDto dto = (TrapdConfigDto) response.getEntity(); + assertEquals(Integer.valueOf(162), dto.getSnmpTrapPort()); + assertEquals("127.0.0.1", dto.getSnmpTrapAddress()); + } + } + @Test public void getShouldReturnNotFoundWhenNoConfigurationExists() { when(m_trapdConfigDao.getConfig()).thenReturn(null); @@ -142,6 +176,530 @@ public void getShouldReturnNotFoundWhenNoConfigurationExists() { } } + @Test + public void saveUserShouldReturnBadRequestWhenPayloadMissing() { + try (Response response = m_trapdRestService.saveTrapdUser(null, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Missing SNMPv3 user in request body.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldPersistUserAndReturnDto() { + TrapdConfiguration existing = new TrapdConfiguration(); + existing.setSnmpTrapPort(1162); + existing.setNewSuspectOnTrap(false); + when(m_trapdConfigDao.getConfig()).thenReturn(existing); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setEngineId("8000000001020304"); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(3); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + user.setPrivacyProtocol("AES"); + user.setPrivacyPassphrase("priv-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof Snmpv3UserDto); + Snmpv3UserDto dto = (Snmpv3UserDto) response.getEntity(); + assertEquals("opennms-user", dto.getSecurityName()); + assertEquals(Integer.valueOf(3), dto.getSecurityLevel()); + assertEquals("AES", dto.getPrivacyProtocol()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + TrapdConfiguration persisted = captor.getValue(); + assertEquals(1, persisted.getSnmpv3UserCount()); + assertEquals("opennms-user", persisted.getSnmpv3User(0).getSecurityName()); + } + + @Test + public void saveUserShouldRejectWhenSecurityNameMissing() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityLevel(1); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityName is required.", response.getEntity()); + } + + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenSecurityLevelThreeMissingPrivacy() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(3); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel 3 requires both auth and privacy credentials.", response.getEntity()); + } + + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldReturnBadRequestWhenValidationFails() { + TrapdConfiguration existing = new TrapdConfiguration(); + existing.setSnmpTrapPort(1162); + existing.setNewSuspectOnTrap(false); + when(m_trapdConfigDao.getConfig()).thenReturn(existing); + whenValidationFailsOnUpdate("user validation failed"); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("user validation failed", response.getEntity()); + } + } + + @Test + public void saveUserShouldReturnServerErrorWhenPersistenceThrows() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("Failed to persist trapd user.", response.getEntity()); + } + } + + @Test + public void saveUserShouldCreateNewConfigWhenNoneExists() { + when(m_trapdConfigDao.getConfig()).thenReturn(null); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(1); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof Snmpv3UserDto); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + TrapdConfiguration persisted = captor.getValue(); + assertEquals(1, persisted.getSnmpv3UserCount()); + assertEquals(162, persisted.getSnmpTrapPort()); + } + + // --- validateSnmpv3UserPayload rule tests --- + + @Test + public void saveUserShouldRejectWhenSecurityLevelOutOfRange() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(5); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel must be between 1 and 3.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenAuthProtocolIsInvalid() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setAuthProtocol("MD2"); + user.setAuthPassphrase("auth-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Unsupported authProtocol.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenPrivacyProtocolIsInvalid() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + user.setPrivacyProtocol("3DES"); + user.setPrivacyPassphrase("priv-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Unsupported privacyProtocol.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenAuthProtocolProvidedWithoutPassphrase() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setAuthProtocol("SHA"); + // no authPassphrase + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("authProtocol and authPassphrase must be provided together.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenPrivacyProtocolProvidedWithoutPassphrase() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + user.setPrivacyProtocol("AES"); + // no privacyPassphrase + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("privacyProtocol and privacyPassphrase must be provided together.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenSecurityLevelOneHasAuthCredentials() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(1); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel 1 does not allow auth or privacy credentials.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenSecurityLevelTwoMissingAuthCredentials() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(2); + // no auth protocol/passphrase + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel 2 requires auth credentials and does not allow privacy credentials.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void saveUserShouldRejectWhenSecurityLevelTwoHasPrivacyCredentials() { + when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(2); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + user.setPrivacyProtocol("AES"); + user.setPrivacyPassphrase("priv-pass"); + + try (Response response = m_trapdRestService.saveTrapdUser(user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel 2 requires auth credentials and does not allow privacy credentials.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void deleteUserShouldReturnBadRequestWhenIndexNull() { + try (Response response = m_trapdRestService.deleteTrapdUser(null, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Valid user index is required.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void deleteUserShouldReturnBadRequestWhenIndexNegative() { + try (Response response = m_trapdRestService.deleteTrapdUser(-1, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Valid user index is required.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void deleteUserShouldReturnNotFoundWhenNoConfig() { + when(m_trapdConfigDao.getConfig()).thenReturn(null); + + try (Response response = m_trapdRestService.deleteTrapdUser(0, null)) { + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + assertEquals("Trapd configuration not found.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void deleteUserShouldReturnBadRequestWhenIndexOutOfRange() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + try (Response response = m_trapdRestService.deleteTrapdUser(5, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(((String) response.getEntity()).contains("out of range")); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void deleteUserShouldRemoveUserAndReturnDto() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User user = new Snmpv3User(); + user.setSecurityName("user-to-delete"); + user.setSecurityLevel(1); + config.addSnmpv3User(user); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + try (Response response = m_trapdRestService.deleteTrapdUser(0, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof Snmpv3UserDto); + Snmpv3UserDto dto = (Snmpv3UserDto) response.getEntity(); + assertEquals("user-to-delete", dto.getSecurityName()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + assertEquals(0, captor.getValue().getSnmpv3UserCount()); + } + + @Test + public void deleteUserShouldReturnBadRequestWhenValidationFails() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User user = new Snmpv3User(); + user.setSecurityName("test-user"); + config.addSnmpv3User(user); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + whenValidationFailsOnUpdate("delete validation error"); + + try (Response response = m_trapdRestService.deleteTrapdUser(0, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("delete validation error", response.getEntity()); + } + } + + @Test + public void deleteUserShouldReturnServerErrorWhenPersistenceThrows() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User user = new Snmpv3User(); + user.setSecurityName("test-user"); + config.addSnmpv3User(user); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + + try (Response response = m_trapdRestService.deleteTrapdUser(0, null)) { + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("Failed to delete trapd user.", response.getEntity()); + } + } + + @Test + public void updateUserShouldReturnBadRequestWhenIndexNull() { + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(null, user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Valid user index is required.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReturnBadRequestWhenIndexNegative() { + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(-1, user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Valid user index is required.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReturnBadRequestWhenPayloadNull() { + try (Response response = m_trapdRestService.updateTrapdUser(0, null, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("Missing SNMPv3 user in request body.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReturnNotFoundWhenNoConfig() { + when(m_trapdConfigDao.getConfig()).thenReturn(null); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(0, user, null)) { + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); + assertEquals("Trapd configuration not found.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReturnBadRequestWhenIndexOutOfRange() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(5, user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(((String) response.getEntity()).contains("out of range")); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReturnBadRequestWhenPayloadValidationFails() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User existing = new Snmpv3User(); + existing.setSecurityName("existing-user"); + config.addSnmpv3User(existing); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + // securityLevel 3 requires privacy — missing privacy intentionally + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + user.setSecurityLevel(3); + user.setAuthProtocol("SHA"); + user.setAuthPassphrase("auth-pass"); + + try (Response response = m_trapdRestService.updateTrapdUser(0, user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("securityLevel 3 requires both auth and privacy credentials.", response.getEntity()); + } + verify(m_trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + } + + @Test + public void updateUserShouldReplaceUserAtIndexAndReturnDto() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User existing = new Snmpv3User(); + existing.setSecurityName("old-user"); + existing.setSecurityLevel(1); + config.addSnmpv3User(existing); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + + Snmpv3UserDto updated = new Snmpv3UserDto(); + updated.setSecurityName("new-user"); + updated.setSecurityLevel(3); + updated.setAuthProtocol("SHA"); + updated.setAuthPassphrase("auth-pass"); + updated.setPrivacyProtocol("AES"); + updated.setPrivacyPassphrase("priv-pass"); + + try (Response response = m_trapdRestService.updateTrapdUser(0, updated, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof Snmpv3UserDto); + Snmpv3UserDto dto = (Snmpv3UserDto) response.getEntity(); + assertEquals("new-user", dto.getSecurityName()); + assertEquals(Integer.valueOf(3), dto.getSecurityLevel()); + assertEquals("AES", dto.getPrivacyProtocol()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + assertEquals(1, captor.getValue().getSnmpv3UserCount()); + assertEquals("new-user", captor.getValue().getSnmpv3User(0).getSecurityName()); + } + + @Test + public void updateUserShouldReturnBadRequestWhenSchemaValidationFails() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User existing = new Snmpv3User(); + existing.setSecurityName("existing-user"); + config.addSnmpv3User(existing); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + whenValidationFailsOnUpdate("schema update error"); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(0, user, null)) { + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertEquals("schema update error", response.getEntity()); + } + } + + @Test + public void updateUserShouldReturnServerErrorWhenPersistenceThrows() { + TrapdConfiguration config = new TrapdConfiguration(); + config.setSnmpTrapPort(162); + config.setNewSuspectOnTrap(false); + Snmpv3User existing = new Snmpv3User(); + existing.setSecurityName("existing-user"); + config.addSnmpv3User(existing); + when(m_trapdConfigDao.getConfig()).thenReturn(config); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(m_trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); + + Snmpv3UserDto user = new Snmpv3UserDto(); + user.setSecurityName("opennms-user"); + + try (Response response = m_trapdRestService.updateTrapdUser(0, user, null)) { + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + assertEquals("Failed to update trapd user.", response.getEntity()); + } + } + @Test public void updateShouldReturnBadRequestWhenPayloadMissing() { try (Response response = m_trapdRestService.updateTrapdConfiguration(null, null)) { From dbab24dc700b0dfab646689fd785d808c51d4441 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Tue, 17 Mar 2026 23:15:14 +0500 Subject: [PATCH 05/41] mappers fixes --- .../netmgt/dao/jaxb/DefaultTrapdConfigDao.java | 6 ++++++ .../org/opennms/web/rest/v2/TrapdRestService.java | 12 ++++++------ .../org/opennms/web/rest/v2/model/Snmpv3UserDto.java | 2 +- .../opennms/web/rest/v2/model/TrapdConfigDto.java | 11 ++++++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java b/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java index 9c49af73da65..a9ae43205383 100644 --- a/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java +++ b/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java @@ -23,6 +23,7 @@ import org.opennms.features.config.service.api.ConfigUpdateInfo; import org.opennms.features.config.service.impl.AbstractCmJaxbConfigDao; +import org.opennms.features.config.service.util.ConfigConvertUtil; import org.opennms.netmgt.config.trapd.TrapdConfiguration; import org.opennms.netmgt.dao.api.TrapdConfigDao; import org.opennms.netmgt.dao.jaxb.callback.ConfigurationReloadEventCallback; @@ -51,6 +52,11 @@ public TrapdConfiguration getConfig() { return this.getConfig(this.getDefaultConfigId()); } + @Override + public void updateConfig(final TrapdConfiguration config) { + this.updateConfig(this.getDefaultConfigId(), ConfigConvertUtil.objectToJson(config), true); + } + @Override public Consumer getUpdateCallback(){ return new ConfigurationReloadEventCallback(eventForwarder, this); diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java index 5c2262c66b08..33929bbe6f4c 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java @@ -70,7 +70,7 @@ public Response uploadTrapdConfiguration(final Attachment attachment, final Secu try { m_trapdConfigDao.updateConfig(config); - return Response.ok(new TrapdConfigDto().toDto(config)).build(); + return Response.ok(TrapdConfigDto.toDto(config)).build(); } catch (ValidationException e) { LOG.warn("Uploaded trapd configuration failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -87,7 +87,7 @@ public Response getTrapdConfiguration(final SecurityContext securityContext) { if (config == null) { return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); } - return Response.ok(new TrapdConfigDto().toDto(config)).build(); + return Response.ok(TrapdConfigDto.toDto(config)).build(); } catch (Exception e) { LOG.error("Failed to retrieve trapd configuration.", e); return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to retrieve trapd configuration.").build(); @@ -110,7 +110,7 @@ public Response updateTrapdConfiguration(TrapdConfigDto config, SecurityContext try { m_trapdConfigDao.updateConfig(updatedConfig); - return Response.ok(new TrapdConfigDto().toDto(m_trapdConfigDao.getConfig())).build(); + return Response.ok(TrapdConfigDto.toDto(m_trapdConfigDao.getConfig())).build(); } catch (ValidationException e) { LOG.warn("Provided trapd configuration failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -155,7 +155,7 @@ public Response saveTrapdUser(Snmpv3UserDto user, SecurityContext securityContex config.addSnmpv3User(snmpv3User); m_trapdConfigDao.updateConfig(config); - return Response.ok(new Snmpv3UserDto().toDto(snmpv3User)).build(); + return Response.ok(Snmpv3UserDto.toDto(snmpv3User)).build(); } catch (ValidationException e) { LOG.warn("Provided SNMPv3 user failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -202,7 +202,7 @@ public Response updateTrapdUser(Integer index, Snmpv3UserDto user, SecurityConte config.setSnmpv3User(index, snmpv3User); m_trapdConfigDao.updateConfig(config); - return Response.ok(new Snmpv3UserDto().toDto(snmpv3User)).build(); + return Response.ok(Snmpv3UserDto.toDto(snmpv3User)).build(); } catch (ValidationException e) { LOG.warn("Updating SNMPv3 user failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); @@ -232,7 +232,7 @@ public Response deleteTrapdUser(Integer index, SecurityContext securityContext) final Snmpv3User removed = config.removeSnmpv3UserAt(index); m_trapdConfigDao.updateConfig(config); - return Response.ok(new Snmpv3UserDto().toDto(removed)).build(); + return Response.ok(Snmpv3UserDto.toDto(removed)).build(); } catch (ValidationException e) { LOG.warn("Removing SNMPv3 user failed schema validation.", e); return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java index 5dc6fbd968ad..7470e5b21baa 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/Snmpv3UserDto.java @@ -89,7 +89,7 @@ public void setPrivacyPassphrase(String privacyPassphrase) { this.privacyPassphrase = privacyPassphrase; } - public Snmpv3UserDto toDto(Snmpv3User user) { + public static Snmpv3UserDto toDto(Snmpv3User user) { if (user == null) { return null; } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java index 3f8f432861fd..e1ebbe990925 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java @@ -24,6 +24,7 @@ import org.opennms.netmgt.config.trapd.Snmpv3User; import org.opennms.netmgt.config.trapd.TrapdConfiguration; +import java.util.Arrays; import java.util.List; public class TrapdConfigDto { @@ -36,7 +37,7 @@ public class TrapdConfigDto { private Integer batchSize; private Integer batchInterval; private Boolean useAddressFromVarbind; - private List snmpv3User; + private List snmpv3User; public String getSnmpTrapAddress() { return snmpTrapAddress; @@ -110,11 +111,11 @@ public void setUseAddressFromVarbind(final Boolean useAddressFromVarbind) { this.useAddressFromVarbind = useAddressFromVarbind; } - public List getSnmpv3User() { return snmpv3User; } + public List getSnmpv3User() { return snmpv3User; } - public void setSnmpv3User(List snmpv3Users) { this.snmpv3User = snmpv3Users; } + public void setSnmpv3User(List snmpv3Users) { this.snmpv3User = snmpv3Users; } - public TrapdConfigDto toDto(final TrapdConfiguration config) { + public static TrapdConfigDto toDto(final TrapdConfiguration config) { TrapdConfigDto dto = new TrapdConfigDto(); dto.setSnmpTrapAddress(config.getSnmpTrapAddress()); dto.setSnmpTrapPort(config.getSnmpTrapPort()); @@ -125,7 +126,7 @@ public TrapdConfigDto toDto(final TrapdConfiguration config) { dto.setBatchSize(config.getBatchSize()); dto.setBatchInterval(config.getBatchInterval()); dto.setUseAddressFromVarbind(config.shouldUseAddressFromVarbind()); - dto.setSnmpv3User(List.of(config.getSnmpv3User())); + dto.setSnmpv3User(Arrays.stream(config.getSnmpv3User()).map(Snmpv3UserDto::toDto).toList()); return dto; } } From 7b025cee3c9f6a779fd3d96d1d91bc50960a3bef Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Wed, 18 Mar 2026 21:34:41 +0500 Subject: [PATCH 06/41] field use address from varbind fix --- .../config/service/config/FakeXsdForTest.java | 14 +++--- .../config/trapd/TrapdConfiguration.java | 14 +++--- .../netmgt/config/TrapdConfigFactory.java | 1 + .../web/rest/v2/model/TrapdConfigDto.java | 9 ++-- .../web/rest/v2/TrapdRestServiceIT.java | 49 +++++++++++++++++++ 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/features/config/test/src/test/java/org/opennms/features/config/service/config/FakeXsdForTest.java b/features/config/test/src/test/java/org/opennms/features/config/service/config/FakeXsdForTest.java index 32446e1020ac..cb0abdd32b28 100644 --- a/features/config/test/src/test/java/org/opennms/features/config/service/config/FakeXsdForTest.java +++ b/features/config/test/src/test/java/org/opennms/features/config/service/config/FakeXsdForTest.java @@ -46,7 +46,7 @@ public class FakeXsdForTest implements Serializable { private static final long serialVersionUID = 2; - public static final boolean DEFAULT_USE_ADDESS_FROM_VARBIND = false; + public static final boolean DEFAULT_USE_ADDRESS_FROM_VARBIND = false; /** * The IP address on which trapd listens for connections. @@ -127,7 +127,7 @@ public class FakeXsdForTest implements Serializable { * SNMPv2 traps. */ @XmlAttribute(name = "use-address-from-varbind", required = false) - private Boolean _useAddessFromVarbind; + private Boolean _useAddressFromVarbind; public FakeXsdForTest() { super(); @@ -198,7 +198,7 @@ public java.util.Enumeration enumerateSnmpv3User( public int hashCode() { return Objects.hash(_snmpTrapAddress, _snmpTrapPort, _has_snmpTrapPort, _newSuspectOnTrap, _snmpv3UserList, - _includeRawMessage, _threads, _queueSize, _batchSize, _batchInterval, _useAddessFromVarbind); + _includeRawMessage, _threads, _queueSize, _batchSize, _batchInterval, _useAddressFromVarbind); } @Override() @@ -218,7 +218,7 @@ public boolean equals(final java.lang.Object obj) { && Objects.equals(_queueSize, other._queueSize) && Objects.equals(_batchSize, other._batchSize) && Objects.equals(_batchInterval, other._batchInterval) - && Objects.equals(_useAddessFromVarbind, other._useAddessFromVarbind); + && Objects.equals(_useAddressFromVarbind, other._useAddressFromVarbind); return equals; } return false; @@ -355,11 +355,11 @@ public boolean isNewSuspectOnTrap( } public boolean shouldUseAddressFromVarbind() { - return _useAddessFromVarbind != null ? _useAddessFromVarbind : DEFAULT_USE_ADDESS_FROM_VARBIND; + return _useAddressFromVarbind != null ? _useAddressFromVarbind : DEFAULT_USE_ADDRESS_FROM_VARBIND; } - public void setUseAddressFromVarbind(Boolean useAddessFromVarbind) { - _useAddessFromVarbind = useAddessFromVarbind; + public void setUseAddressFromVarbind(Boolean useAddressFromVarbind) { + _useAddressFromVarbind = useAddressFromVarbind; } /** diff --git a/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java index 4e9f8e86ffc3..6d600a9c8be1 100644 --- a/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java +++ b/opennms-config-jaxb/src/main/java/org/opennms/netmgt/config/trapd/TrapdConfiguration.java @@ -49,7 +49,7 @@ public class TrapdConfiguration implements Serializable { private static final long serialVersionUID = 2; - public static final boolean DEFAULT_USE_ADDESS_FROM_VARBIND = false; + public static final boolean DEFAULT_USE_ADDRESS_FROM_VARBIND = false; /** * The IP address on which trapd listens for connections. @@ -132,7 +132,7 @@ public class TrapdConfiguration implements Serializable { * SNMPv2 traps. */ @XmlAttribute(name="use-address-from-varbind", required=false) - private Boolean useAddessFromVarbind; + private Boolean useAddressFromVarbind; public TrapdConfiguration() { super(); @@ -205,7 +205,7 @@ public java.util.Enumeration enumerateSnmpv3User( public int hashCode() { return Objects.hash(snmpTrapAddress, snmpTrapPort, hasSnmpTrapPort, newSuspectOnTrap, snmpv3User, - includeRawMessage, threads, queueSize, batchSize, batchInterval, useAddessFromVarbind); + includeRawMessage, threads, queueSize, batchSize, batchInterval, useAddressFromVarbind); } @Override() @@ -225,7 +225,7 @@ public boolean equals(final java.lang.Object obj) { && Objects.equals(queueSize, other.queueSize) && Objects.equals(batchSize, other.batchSize) && Objects.equals(batchInterval, other.batchInterval) - && Objects.equals(useAddessFromVarbind, other.useAddessFromVarbind); + && Objects.equals(useAddressFromVarbind, other.useAddressFromVarbind); return equals; } return false; @@ -363,11 +363,11 @@ public boolean isNewSuspectOnTrap( } public boolean shouldUseAddressFromVarbind() { - return useAddessFromVarbind != null ? useAddessFromVarbind : DEFAULT_USE_ADDESS_FROM_VARBIND; + return useAddressFromVarbind != null ? useAddressFromVarbind : DEFAULT_USE_ADDRESS_FROM_VARBIND; } - public void setUseAddressFromVarbind(Boolean useAddessFromVarbind) { - this.useAddessFromVarbind = useAddessFromVarbind; + public void setUseAddressFromVarbind(Boolean useAddressFromVarbind) { + this.useAddressFromVarbind = useAddressFromVarbind; } /** diff --git a/opennms-config/src/main/java/org/opennms/netmgt/config/TrapdConfigFactory.java b/opennms-config/src/main/java/org/opennms/netmgt/config/TrapdConfigFactory.java index 4c28c363ee3c..b4adbc64b299 100644 --- a/opennms-config/src/main/java/org/opennms/netmgt/config/TrapdConfigFactory.java +++ b/opennms-config/src/main/java/org/opennms/netmgt/config/TrapdConfigFactory.java @@ -221,6 +221,7 @@ public void update(TrapdConfig config) { m_config.setSnmpTrapAddress(config.getSnmpTrapAddress()); m_config.setSnmpTrapPort(config.getSnmpTrapPort()); m_config.setNewSuspectOnTrap(config.getNewSuspectOnTrap()); + m_config.setUseAddressFromVarbind(config.shouldUseAddressFromVarbind()); m_config.setQueueSize(config.getQueueSize()); m_config.setBatchSize(config.getBatchSize()); m_config.setBatchInterval(config.getBatchIntervalMs()); diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java index e1ebbe990925..7349f0ef682d 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/model/TrapdConfigDto.java @@ -21,7 +21,6 @@ */ package org.opennms.web.rest.v2.model; -import org.opennms.netmgt.config.trapd.Snmpv3User; import org.opennms.netmgt.config.trapd.TrapdConfiguration; import java.util.Arrays; @@ -111,9 +110,13 @@ public void setUseAddressFromVarbind(final Boolean useAddressFromVarbind) { this.useAddressFromVarbind = useAddressFromVarbind; } - public List getSnmpv3User() { return snmpv3User; } + public List getSnmpv3User() { + return snmpv3User; + } - public void setSnmpv3User(List snmpv3Users) { this.snmpv3User = snmpv3Users; } + public void setSnmpv3User(List snmpv3Users) { + this.snmpv3User = snmpv3Users; + } public static TrapdConfigDto toDto(final TrapdConfiguration config) { TrapdConfigDto dto = new TrapdConfigDto(); diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java index 3855895f853e..c0c11c0d1090 100644 --- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java @@ -121,6 +121,25 @@ public void uploadShouldPersistValidXmlAndReturnDto() { assertEquals(10163, captor.getValue().getSnmpTrapPort()); } + @Test + public void uploadShouldPersistUseAddressFromVarbindWhenProvidedInXml() { + Attachment attachment = mock(Attachment.class); + when(attachment.getObject(InputStream.class)).thenReturn( + new ByteArrayInputStream(validTrapdConfigXmlWithUseAddressFromVarbind().getBytes(StandardCharsets.UTF_8)) + ); + + try (Response response = m_trapdRestService.uploadTrapdConfiguration(attachment, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + assertTrue(response.getEntity() instanceof TrapdConfigDto); + TrapdConfigDto dto = (TrapdConfigDto) response.getEntity(); + assertEquals(Boolean.TRUE, dto.getUseAddressFromVarbind()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + assertTrue(captor.getValue().shouldUseAddressFromVarbind()); + } + @Test public void uploadShouldReturnBadRequestWhenValidationFails() { Attachment attachment = mock(Attachment.class); @@ -737,6 +756,29 @@ public void updateShouldMergePayloadAndPersist() { assertEquals(4, persisted.getThreads()); } + @Test + public void updateShouldPersistUseAddressFromVarbindWhenProvided() { + TrapdConfiguration existing = new TrapdConfiguration(); + existing.setSnmpTrapAddress("127.0.0.1"); + existing.setSnmpTrapPort(1162); + existing.setNewSuspectOnTrap(false); + existing.setUseAddressFromVarbind(false); + when(m_trapdConfigDao.getConfig()).thenReturn(existing, existing); + + TrapdConfigDto payload = new TrapdConfigDto(); + payload.setUseAddressFromVarbind(Boolean.TRUE); + + try (Response response = m_trapdRestService.updateTrapdConfiguration(payload, null)) { + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + TrapdConfigDto dto = (TrapdConfigDto) response.getEntity(); + assertEquals(Boolean.TRUE, dto.getUseAddressFromVarbind()); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); + verify(m_trapdConfigDao).updateConfig(captor.capture()); + assertTrue(captor.getValue().shouldUseAddressFromVarbind()); + } + @Test public void updateShouldReturnBadRequestWhenValidationFails() { when(m_trapdConfigDao.getConfig()).thenReturn(new TrapdConfiguration()); @@ -781,5 +823,12 @@ private static String validTrapdConfigXml() { + "include-raw-message=\"false\" threads=\"0\" queue-size=\"10000\" " + "batch-size=\"1000\" batch-interval=\"500\"/>"; } + + private static String validTrapdConfigXmlWithUseAddressFromVarbind() { + return ""; + } } From 2dc702d0454ca29b0beffb327b1e63b7792947ee Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Wed, 18 Mar 2026 21:36:10 +0500 Subject: [PATCH 07/41] Trapd UI rest endpoint integration --- .../GeneralConfiguration.vue | 132 +++++++++++++++- ui/src/containers/TrapConfiguration.vue | 56 ++++++- ui/src/services/index.ts | 14 ++ ui/src/services/trapdConfigurationService.ts | 145 ++++++++++++++++++ ui/src/stores/trapConfigStore.ts | 9 ++ ui/src/types/trapConfig.d.ts | 34 ++++ 6 files changed, 386 insertions(+), 4 deletions(-) create mode 100644 ui/src/services/trapdConfigurationService.ts diff --git a/ui/src/components/TrapConfiguration/GeneralConfiguration.vue b/ui/src/components/TrapConfiguration/GeneralConfiguration.vue index f8870e0b533a..b4a496b9f08c 100644 --- a/ui/src/components/TrapConfiguration/GeneralConfiguration.vue +++ b/ui/src/components/TrapConfiguration/GeneralConfiguration.vue @@ -10,13 +10,17 @@
@@ -59,6 +63,8 @@ @@ -67,6 +73,8 @@ @@ -75,6 +83,8 @@ @@ -83,6 +93,8 @@ @@ -97,6 +109,8 @@ Update Changes @@ -105,15 +119,30 @@ - diff --git a/ui/src/services/index.ts b/ui/src/services/index.ts index c749b96573eb..4ff81dcc13ab 100644 --- a/ui/src/services/index.ts +++ b/ui/src/services/index.ts @@ -76,6 +76,14 @@ import { addZenithRegistration, getZenithRegistrations } from './zenithConnectService' +import { + deleteTrapdUser, + getTrapdConfiguration, + saveTrapdUser, + updateTrapdConfiguration, + updateTrapdUser, + uploadTrapdConfiguration +} from './trapdConfigurationService' export default { search, @@ -135,5 +143,11 @@ export default { setUsageStatisticsStatus, addZenithRegistration, getZenithRegistrations, + uploadTrapdConfiguration, + getTrapdConfiguration, + updateTrapdConfiguration, + saveTrapdUser, + updateTrapdUser, + deleteTrapdUser, performLogout } diff --git a/ui/src/services/trapdConfigurationService.ts b/ui/src/services/trapdConfigurationService.ts new file mode 100644 index 000000000000..6f0340c7a40b --- /dev/null +++ b/ui/src/services/trapdConfigurationService.ts @@ -0,0 +1,145 @@ +import axios from 'axios' + +import type { SnmpV3User, TrapConfig } from '@/types/trapConfig' + +import { v2 } from './axiosInstances' + +const endpoint = '/trapd' + +export type TrapdConfigurationUpdatePayload = Partial> + +const getTrapdServiceErrorMessage = (error: unknown, fallbackMessage: string): string => { + if (axios.isAxiosError(error)) { + const responseData = error.response?.data + + if (typeof responseData === 'string' && responseData.trim().length > 0) { + return responseData + } + + if ( + responseData && + typeof responseData === 'object' && + 'message' in responseData && + typeof responseData.message === 'string' && + responseData.message.trim().length > 0 + ) { + return responseData.message + } + + if (typeof error.message === 'string' && error.message.trim().length > 0) { + return error.message + } + } + + if (error instanceof Error && error.message.trim().length > 0) { + return error.message + } + + return fallbackMessage +} + +const throwTrapdServiceError = (error: unknown, fallbackMessage: string): never => { + console.error(fallbackMessage, error) + throw new Error(getTrapdServiceErrorMessage(error, fallbackMessage)) +} + +export const uploadTrapdConfiguration = async (file: File): Promise => { + const formData = new FormData() + formData.append('upload', file) + + try { + const response = await v2.post(`${endpoint}/upload`, formData) + + if (response.status === 204) { + return null + } + + if (response.status === 200) { + return response.data as TrapConfig + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to upload trapd configuration.') + } +} + +export const getTrapdConfiguration = async (): Promise => { + try { + const response = await v2.get(`${endpoint}/get-config`) + + if (response.status === 200) { + return response.data as TrapConfig + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to retrieve trapd configuration.') + } +} + +export const updateTrapdConfiguration = async ( + payload: TrapdConfigurationUpdatePayload +): Promise => { + try { + const response = await v2.put(`${endpoint}/update-config`, payload) + + if (response.status === 200) { + return response.data as TrapConfig + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to update trapd configuration.') + } +} + +export const saveTrapdUser = async (user: SnmpV3User): Promise => { + try { + const response = await v2.post(`${endpoint}/save-user`, user) + + if (response.status === 200) { + return response.data as SnmpV3User + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to save trapd user.') + } +} + +export const updateTrapdUser = async (index: number, user: SnmpV3User): Promise => { + try { + const response = await v2.put(`${endpoint}/update-user`, user, { + params: { + index + } + }) + + if (response.status === 200) { + return response.data as SnmpV3User + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to update trapd user.') + } +} + +export const deleteTrapdUser = async (index: number): Promise => { + try { + const response = await v2.delete(`${endpoint}/delete-user`, { + params: { + index + } + }) + + if (response.status === 200) { + return response.data as SnmpV3User + } + + throw new Error(`Unexpected response status: ${response.status}`) + } catch (error) { + return throwTrapdServiceError(error, 'Failed to delete trapd user.') + } +} \ No newline at end of file diff --git a/ui/src/stores/trapConfigStore.ts b/ui/src/stores/trapConfigStore.ts index b968e9cc68a4..eb54d3db63b0 100644 --- a/ui/src/stores/trapConfigStore.ts +++ b/ui/src/stores/trapConfigStore.ts @@ -1,3 +1,4 @@ +import { getTrapdConfiguration } from '@/services/trapdConfigurationService' import { CreateEditMode } from '@/types' import { TrapConfigStoreState } from '@/types/trapConfig' import { defineStore } from 'pinia' @@ -5,6 +6,8 @@ import { defineStore } from 'pinia' export const useTrapConfigStore = defineStore('useTrapConfigStore', { state: (): TrapConfigStoreState => ({ isLoading: false, + trapdConfig: null, + SnmpV3Users: [], activeTab: 0, credentialDrawerState: { visible: false @@ -15,6 +18,12 @@ export const useTrapConfigStore = defineStore('useTrapConfigStore', { } }), actions: { + async fetchTrapConfig() { + // Implementation for fetching trap configuration goes here + const response = await getTrapdConfiguration() + this.trapdConfig = response + this.SnmpV3Users = response.snmpv3User + }, openCredentialDrawer() { this.credentialDrawerState.visible = true }, diff --git a/ui/src/types/trapConfig.d.ts b/ui/src/types/trapConfig.d.ts index e09be1d468cb..6e4a1940676a 100644 --- a/ui/src/types/trapConfig.d.ts +++ b/ui/src/types/trapConfig.d.ts @@ -2,6 +2,8 @@ import { CreateEditMode } from '.' export interface TrapConfigStoreState { isLoading: boolean + trapdConfig: TrapConfig | null + SnmpV3Users: SnmpV3User[] activeTab: number credentialDrawerState: { visible: boolean @@ -12,3 +14,35 @@ export interface TrapConfigStoreState { } } +export interface TrapConfig { + snmpTrapAddress: string + snmpTrapPort: number + newSuspectOnTrap: boolean + includeRawMessage: boolean + threads: number + queueSize: number + batchSize: number + batchInterval: number + useAddressFromVarbind: boolean + snmpv3User: SnmpV3User[] +} + +export interface SnmpV3User { + engineId: string | null + securityName: string + securityLevel: number + authProtocol: string | null + authPassphrase: string | null + privacyProtocol: string | null + privacyPassphrase: string | null +} + +export interface TrapdConfigurationError { + port?: string + bindAddress?: string + threads?: string + queueSize?: string + batchSize?: string + batchInterval?: string + snmpv3User?: string +} \ No newline at end of file From 0622433047dabd3d0f8b334eb89c20c95620c84c Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Thu, 19 Mar 2026 02:04:40 +0500 Subject: [PATCH 08/41] new changes --- .../TrapConfiguration/CreateSnmpV3User.vue | 256 ++++++++++++++---- .../Dialog/DeleteUserConfirmationDialog.vue | 55 ++++ .../{ => Drawer}/SearchExistingCredential.vue | 2 +- .../GeneralConfiguration.vue | 28 +- .../SnmpV3UserManagement.vue | 70 +++-- .../components/TrapConfiguration/contants.ts | 32 +++ ui/src/mappers/trapdConfig.mapper.ts | 52 ++++ ui/src/services/trapdConfigurationService.ts | 13 +- ui/src/stores/trapConfigStore.ts | 7 +- ui/src/types/trapConfig.d.ts | 13 +- 10 files changed, 436 insertions(+), 92 deletions(-) create mode 100644 ui/src/components/TrapConfiguration/Dialog/DeleteUserConfirmationDialog.vue rename ui/src/components/TrapConfiguration/{ => Drawer}/SearchExistingCredential.vue (98%) create mode 100644 ui/src/components/TrapConfiguration/contants.ts create mode 100644 ui/src/mappers/trapdConfig.mapper.ts diff --git a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue index 9eddeda31578..7229ee89d3db 100644 --- a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue +++ b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue @@ -21,21 +21,19 @@
-
-
@@ -46,62 +44,68 @@
-
+
-
-
+
- @@ -112,17 +116,18 @@ @@ -130,21 +135,178 @@ + diff --git a/ui/src/components/TrapConfiguration/SearchExistingCredential.vue b/ui/src/components/TrapConfiguration/Drawer/SearchExistingCredential.vue similarity index 98% rename from ui/src/components/TrapConfiguration/SearchExistingCredential.vue rename to ui/src/components/TrapConfiguration/Drawer/SearchExistingCredential.vue index 860a6f691bb9..a096cbee5ecb 100644 --- a/ui/src/components/TrapConfiguration/SearchExistingCredential.vue +++ b/ui/src/components/TrapConfiguration/Drawer/SearchExistingCredential.vue @@ -87,13 +87,13 @@ + diff --git a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue index a83f13651856..d60d6e4ed329 100644 --- a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue +++ b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue @@ -73,13 +73,10 @@ v-model="authPassphrase" :error="error.authPassphrase" /> - - - + />
- - - + />
@@ -144,11 +138,11 @@ import { CreateEditMode } from '@/types' import type { SnmpV3UserError } from '@/types/trapConfig' import { FeatherButton } from '@featherds/button' import { FeatherIcon } from '@featherds/icon' -import Security from '@featherds/icon/hardware/Security' import ChevronLeft from '@featherds/icon/navigation/ChevronLeft' import { FeatherInput } from '@featherds/input' import { FeatherSelect, ISelectItemType } from '@featherds/select' import TableCard from '../Common/TableCard.vue' +import ScvInputIcon from '../SCV/ScvInputIcon.vue' import SearchExistingCredential from './Drawer/SearchExistingCredential.vue' const store = useTrapConfigStore() diff --git a/ui/tests/components/TrapConfiguration/CreateSnmpV3User.test.ts b/ui/tests/components/TrapConfiguration/CreateSnmpV3User.test.ts new file mode 100644 index 000000000000..f51d5b4df874 --- /dev/null +++ b/ui/tests/components/TrapConfiguration/CreateSnmpV3User.test.ts @@ -0,0 +1,294 @@ +import CreateSnmpV3User from '@/components/TrapConfiguration/CreateSnmpV3User.vue' +import { AUTH_PROTOCOL_OPTIONS, SECURITY_LEVEL_OPTIONS } from '@/lib/trapdValidator' +import { mapUserToServer } from '@/mappers/trapdConfig.mapper' +import { saveTrapdUser, updateTrapdUser } from '@/services/trapdConfigurationService' +import { useTrapConfigStore } from '@/stores/trapConfigStore' +import { CreateEditMode } from '@/types' +import type { SnmpV3User } from '@/types/trapConfig' +import { createTestingPinia } from '@pinia/testing' +import { flushPromises, mount } from '@vue/test-utils' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, nextTick } from 'vue' + +const { showSnackBarMock } = vi.hoisted(() => ({ + showSnackBarMock: vi.fn() +})) + +vi.mock('@/composables/useSnackbar', () => ({ + default: () => ({ + showSnackBar: showSnackBarMock + }) +})) + +vi.mock('@/mappers/trapdConfig.mapper', () => ({ + mapUserToServer: vi.fn() +})) + +vi.mock('@/services/trapdConfigurationService', () => ({ + saveTrapdUser: vi.fn(), + updateTrapdUser: vi.fn() +})) + +const FeatherInputStub = defineComponent({ + name: 'FeatherInput', + props: { + modelValue: { + type: String, + default: '' + }, + label: { + type: String, + default: '' + }, + dataTest: { + type: String, + default: '' + } + }, + emits: ['update:modelValue'], + template: '' +}) + +describe('CreateSnmpV3User.vue', () => { + let store: ReturnType + const mapUserToServerMock = vi.mocked(mapUserToServer) + const saveTrapdUserMock = vi.mocked(saveTrapdUser) + const updateTrapdUserMock = vi.mocked(updateTrapdUser) + + const selectedUser: SnmpV3User = { + engineId: null, + securityName: 'existing-user', + securityLevel: 2, + authProtocol: 'MD5', + authPassphrase: 'masked-auth', + privacyProtocol: null, + privacyPassphrase: null + } + + const mountComponent = () => { + return mount(CreateSnmpV3User, { + global: { + stubs: { + TableCard: { + template: '
' + }, + SearchExistingCredential: true, + FeatherIcon: true, + FeatherInput: FeatherInputStub, + 'feather-input': FeatherInputStub, + FeatherSelect: true, + 'feather-select': true, + ScvInputIcon: { + emits: ['click'], + template: '' + }, + 'feather-button': { + props: ['dataTest', 'disabled'], + emits: ['click'], + template: '' + } + } + } + }) + } + + const setInputValue = async (wrapper: ReturnType, dataTest: string, value: string) => { + const input = wrapper.find(`input[data-test="${dataTest}"]`) + expect(input.exists()).toBe(true) + await input.setValue(value) + } + + const setBindingValue = async (wrapper: ReturnType, key: string, value: any) => { + ;(wrapper.vm as any)[key] = value + await nextTick() + } + + const clickButton = async (wrapper: ReturnType, dataTest: string) => { + const button = wrapper.findComponent(`[data-test="${dataTest}"]`) + expect(button.exists()).toBe(true) + await (button as any).vm.$emit('click') + await flushPromises() + } + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia( + createTestingPinia({ + stubActions: false, + createSpy: vi.fn + }) + ) + + store = useTrapConfigStore() + store.createUserDrawerState.visible = true + store.createUserDrawerState.mode = CreateEditMode.Create + store.createUserDrawerState.selectedUserIndex = -1 + store.SnmpV3Users = [selectedUser] + + store.fetchTrapConfig = vi.fn().mockResolvedValue(undefined) + store.closeCreateUserDrawer = vi.fn() + store.openCredentialDrawer = vi.fn() + + mapUserToServerMock.mockImplementation((payload) => payload as SnmpV3User) + saveTrapdUserMock.mockResolvedValue(undefined) + updateTrapdUserMock.mockResolvedValue(undefined) + }) + + it('does not render when drawer is hidden', () => { + store.createUserDrawerState.visible = false + const wrapper = mountComponent() + + expect(wrapper.find('[data-test="create-snmpv3-user"]').exists()).toBe(false) + }) + + it('renders create mode with heading and action buttons', () => { + const wrapper = mountComponent() + + expect(wrapper.find('h3').text()).toBe('New SNMPv3 User Management') + expect(wrapper.find('[data-test="create-user-button"]').text()).toContain('Create User') + expect(wrapper.find('[data-test="cancel-button"]').exists()).toBe(true) + }) + + it('renders update label and preloads security name in edit mode', async () => { + store.createUserDrawerState.mode = CreateEditMode.Edit + store.createUserDrawerState.selectedUserIndex = 0 + + const wrapper = mountComponent() + await nextTick() + + expect(wrapper.find('[data-test="create-user-button"]').text()).toContain('Update User') + expect((wrapper.find('input[data-test="security-name-input"]').element as HTMLInputElement).value).toBe('existing-user') + }) + + it('calls closeCreateUserDrawer from back and cancel buttons', async () => { + const wrapper = mountComponent() + + await wrapper.findAll('button')[0].trigger('click') + await wrapper.find('[data-test="cancel-button"]').trigger('click') + + expect(store.closeCreateUserDrawer).toHaveBeenCalledTimes(2) + }) + + it('opens credential drawer from auth passphrase button in edit mode', async () => { + store.createUserDrawerState.mode = CreateEditMode.Edit + store.createUserDrawerState.selectedUserIndex = 0 + + const wrapper = mountComponent() + await nextTick() + + await wrapper.find('[data-test="auth-passphrase-save-button"]').trigger('click') + + expect(store.openCredentialDrawer).toHaveBeenCalledTimes(1) + }) + + it('creates user successfully in create mode', async () => { + const wrapper = mountComponent() + + await setInputValue(wrapper, 'security-name-input', 'new-user') + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[0]) + await clickButton(wrapper, 'create-user-button') + + expect(mapUserToServerMock).toHaveBeenCalledWith(expect.objectContaining({ + securityName: 'new-user', + securityLevel: expect.any(Number) + })) + expect(saveTrapdUserMock).toHaveBeenCalledTimes(1) + expect(updateTrapdUserMock).not.toHaveBeenCalled() + expect(store.fetchTrapConfig).toHaveBeenCalledTimes(1) + expect(store.closeCreateUserDrawer).toHaveBeenCalledTimes(1) + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'SNMPv3 user created successfully.' }) + }) + + it('updates user successfully in edit mode', async () => { + store.createUserDrawerState.mode = CreateEditMode.Edit + store.createUserDrawerState.selectedUserIndex = 0 + + const wrapper = mountComponent() + await nextTick() + + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[1]) + await setBindingValue(wrapper, 'authProtocol', AUTH_PROTOCOL_OPTIONS[0]) + await setBindingValue(wrapper, 'authPassphrase', 'masked-auth') + await clickButton(wrapper, 'create-user-button') + + expect(updateTrapdUserMock).toHaveBeenCalledWith('existing-user', expect.any(Object)) + expect(saveTrapdUserMock).not.toHaveBeenCalled() + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'SNMPv3 user updated successfully.' }) + }) + + it('shows explicit edit error when selected user has missing securityName', async () => { + store.SnmpV3Users = [{ ...selectedUser, securityName: '' }] + store.createUserDrawerState.mode = CreateEditMode.Edit + store.createUserDrawerState.selectedUserIndex = 0 + + const wrapper = mountComponent() + await nextTick() + + await setInputValue(wrapper, 'security-name-input', 'replacement-name') + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[1]) + await setBindingValue(wrapper, 'authProtocol', AUTH_PROTOCOL_OPTIONS[0]) + await setBindingValue(wrapper, 'authPassphrase', 'masked-auth') + await clickButton(wrapper, 'create-user-button') + + expect(updateTrapdUserMock).not.toHaveBeenCalled() + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Unable to determine the selected SNMPv3 user to update.', error: true }) + }) + + it('shows service error when saveTrapdUser throws Error', async () => { + saveTrapdUserMock.mockRejectedValue(new Error('save failed')) + const wrapper = mountComponent() + + await setInputValue(wrapper, 'security-name-input', 'new-user') + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[0]) + await clickButton(wrapper, 'create-user-button') + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'save failed', error: true }) + }) + + it('shows generic service error when saveTrapdUser throws non-Error', async () => { + saveTrapdUserMock.mockRejectedValue('boom') + const wrapper = mountComponent() + + await setInputValue(wrapper, 'security-name-input', 'new-user') + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[0]) + await clickButton(wrapper, 'create-user-button') + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to save SNMPv3 user.', error: true }) + }) + + it('prevents duplicate create requests while saving is in progress', async () => { + let resolveSave: () => void = () => undefined + saveTrapdUserMock.mockImplementation( + () => new Promise((resolve) => { + resolveSave = resolve + }) + ) + const wrapper = mountComponent() + + await setInputValue(wrapper, 'security-name-input', 'new-user') + await setBindingValue(wrapper, 'securityLevel', SECURITY_LEVEL_OPTIONS[0]) + + await clickButton(wrapper, 'create-user-button') + await clickButton(wrapper, 'create-user-button') + + expect(saveTrapdUserMock).toHaveBeenCalledTimes(1) + + resolveSave() + await flushPromises() + }) + + it('shows validation message when trying to save with empty security name', async () => { + const wrapper = mountComponent() + + await clickButton(wrapper, 'create-user-button') + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Please fix validation errors before saving.', error: true }) + expect(saveTrapdUserMock).not.toHaveBeenCalled() + }) +}) diff --git a/ui/tests/components/TrapConfiguration/GeneralConfiguration.test.ts b/ui/tests/components/TrapConfiguration/GeneralConfiguration.test.ts new file mode 100644 index 000000000000..d213e013cefc --- /dev/null +++ b/ui/tests/components/TrapConfiguration/GeneralConfiguration.test.ts @@ -0,0 +1,332 @@ +import GeneralConfiguration from '@/components/TrapConfiguration/GeneralConfiguration.vue' +import { MAX_PORT, MIN_PORT } from '@/lib/trapdValidator' +import { updateTrapdConfiguration } from '@/services/trapdConfigurationService' +import { useTrapConfigStore } from '@/stores/trapConfigStore' +import type { TrapConfig } from '@/types/trapConfig' +import { createTestingPinia } from '@pinia/testing' +import { flushPromises, mount } from '@vue/test-utils' +import { cloneDeep } from 'lodash' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { nextTick } from 'vue' + +const { showSnackBarMock } = vi.hoisted(() => ({ + showSnackBarMock: vi.fn() +})) + +vi.mock('@/composables/useSnackbar', () => ({ + default: () => ({ + showSnackBar: showSnackBarMock + }) +})) + +vi.mock('@/services/trapdConfigurationService', () => ({ + updateTrapdConfiguration: vi.fn() +})) + +describe('GeneralConfiguration.vue', () => { + let store: ReturnType + const updateTrapdConfigurationMock = vi.mocked(updateTrapdConfiguration) + + const baseTrapConfig: TrapConfig = { + snmpTrapAddress: '192.168.1.10', + snmpTrapPort: 162, + newSuspectOnTrap: true, + includeRawMessage: false, + threads: 4, + queueSize: 5000, + batchSize: 250, + batchInterval: 750, + useAddressFromVarbind: true, + snmpv3User: [] + } + + const mountComponent = () => { + return mount(GeneralConfiguration, { + global: { + stubs: { + TableCard: { + template: '
' + }, + FeatherExpansionPanel: { + props: ['title'], + template: '
' + }, + FeatherInput: true, + 'feather-input': true, + FeatherButton: true, + 'feather-button': true, + SwitchRender: true, + 'switch-render': true + } + } + }) + } + + const setBindingValue = async ( + wrapper: ReturnType, + key: string, + value: string | number | boolean + ) => { + ;(wrapper.vm as any)[key] = value + await nextTick() + } + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia( + createTestingPinia({ + stubActions: false, + createSpy: vi.fn + }) + ) + + store = useTrapConfigStore() + store.trapdConfig = cloneDeep(baseTrapConfig) + store.fetchTrapConfig = vi.fn().mockResolvedValue(undefined) + + updateTrapdConfigurationMock.mockResolvedValue(undefined) + }) + + it('renders the section labels and loads the current trap configuration values from the store', () => { + const wrapper = mountComponent() + + expect(wrapper.text()).toContain('TrapD Listener Settings') + expect(wrapper.text()).toContain('Update Changes') + expect((wrapper.vm as any).port).toBe(162) + expect((wrapper.vm as any).bindAddress).toBe('192.168.1.10') + expect((wrapper.vm as any).status).toBe(true) + expect((wrapper.vm as any).trapMessageStatus).toBe(false) + expect((wrapper.vm as any).trapSourceAddressStatus).toBe(true) + expect((wrapper.vm as any).threads).toBe(4) + expect((wrapper.vm as any).queueSize).toBe(5000) + expect((wrapper.vm as any).batchSize).toBe(250) + expect((wrapper.vm as any).batchInterval).toBe(750) + expect((wrapper.vm as any).trapConfigError).toEqual({}) + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('reloads form state when trapdConfig changes in the store', async () => { + const wrapper = mountComponent() + + store.trapdConfig = { + ...cloneDeep(baseTrapConfig), + snmpTrapPort: 10162, + snmpTrapAddress: '*', + newSuspectOnTrap: false, + includeRawMessage: true, + useAddressFromVarbind: false, + threads: 0, + queueSize: 10000, + batchSize: 1000, + batchInterval: 500 + } + await nextTick() + await nextTick() + + expect((wrapper.vm as any).port).toBe(10162) + expect((wrapper.vm as any).bindAddress).toBe('*') + expect((wrapper.vm as any).status).toBe(false) + expect((wrapper.vm as any).trapMessageStatus).toBe(true) + expect((wrapper.vm as any).trapSourceAddressStatus).toBe(false) + expect((wrapper.vm as any).threads).toBe(0) + expect((wrapper.vm as any).queueSize).toBe(10000) + expect((wrapper.vm as any).batchSize).toBe(1000) + expect((wrapper.vm as any).batchInterval).toBe(500) + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('enables save when a valid field changes and disables it again when reverted', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', 163) + expect((wrapper.vm as any).isSaveDisabled).toBe(false) + + await setBindingValue(wrapper, 'port', 162) + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('toggles all switch values through the component handlers', () => { + const wrapper = mountComponent() + + ;(wrapper.vm as any).onChangeStatus() + ;(wrapper.vm as any).onChangeTrapMessageStatus() + ;(wrapper.vm as any).onChangeTrapSourceAddressStatus() + + expect((wrapper.vm as any).status).toBe(false) + expect((wrapper.vm as any).trapMessageStatus).toBe(true) + expect((wrapper.vm as any).trapSourceAddressStatus).toBe(false) + }) + + it('submits the updated payload successfully and refreshes the store', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', '10162') + await setBindingValue(wrapper, 'bindAddress', '*') + await setBindingValue(wrapper, 'status', false) + await setBindingValue(wrapper, 'trapMessageStatus', true) + await setBindingValue(wrapper, 'trapSourceAddressStatus', false) + await setBindingValue(wrapper, 'threads', '2') + await setBindingValue(wrapper, 'queueSize', '6000') + await setBindingValue(wrapper, 'batchSize', '300') + await setBindingValue(wrapper, 'batchInterval', '900') + + await (wrapper.vm as any).updateConfig() + await flushPromises() + + expect(updateTrapdConfigurationMock).toHaveBeenCalledWith({ + snmpTrapPort: 10162, + snmpTrapAddress: '*', + newSuspectOnTrap: false, + useAddressFromVarbind: false, + includeRawMessage: true, + threads: 2, + queueSize: 6000, + batchSize: 300, + batchInterval: 900 + }) + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Trap configuration updated successfully.' }) + expect(store.fetchTrapConfig).toHaveBeenCalledTimes(1) + expect((wrapper.vm as any).isSaving).toBe(false) + }) + + it('shows the service error message when update fails with an Error', async () => { + updateTrapdConfigurationMock.mockRejectedValue(new Error('update failed')) + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', 163) + await (wrapper.vm as any).updateConfig() + await flushPromises() + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'update failed', error: true }) + expect(store.fetchTrapConfig).not.toHaveBeenCalled() + expect((wrapper.vm as any).isSaving).toBe(false) + }) + + it('shows a generic error message when update fails with a non-Error value', async () => { + updateTrapdConfigurationMock.mockRejectedValue('boom') + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', 163) + await (wrapper.vm as any).updateConfig() + await flushPromises() + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to update trap configuration.', error: true }) + }) + + it('sets isSaving during an in-flight request and clears it after completion', async () => { + let resolveRequest: (() => void) | undefined + updateTrapdConfigurationMock.mockImplementation( + () => + new Promise((resolve) => { + resolveRequest = resolve + }) + ) + + const wrapper = mountComponent() + await setBindingValue(wrapper, 'port', 163) + + const pendingSave = (wrapper.vm as any).updateConfig() + await nextTick() + + expect((wrapper.vm as any).isSaving).toBe(true) + + resolveRequest?.() + await pendingSave + await flushPromises() + + expect((wrapper.vm as any).isSaving).toBe(false) + }) + + it(`shows a validation error when port is less than ${MIN_PORT}`, async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', 0) + + expect((wrapper.vm as any).trapConfigError.port).toBe(`Port must be between ${MIN_PORT} and ${MAX_PORT}.`) + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it(`shows a validation error when port is greater than ${MAX_PORT}`, async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'port', 65536) + + expect((wrapper.vm as any).trapConfigError.port).toBe(`Port must be between ${MIN_PORT} and ${MAX_PORT}.`) + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('shows a validation error when bind address is empty', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'bindAddress', '') + + expect((wrapper.vm as any).trapConfigError.bindAddress).toBe('Bind Address cannot be empty.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('shows a validation error when bind address is not * or a valid IPv4 address', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'bindAddress', 'localhost') + + expect((wrapper.vm as any).trapConfigError.bindAddress).toBe('Bind Address must be * or a valid IP address.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('accepts wildcard bind address as a valid value', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'bindAddress', '*') + + expect((wrapper.vm as any).trapConfigError.bindAddress).toBeUndefined() + expect((wrapper.vm as any).isSaveDisabled).toBe(false) + }) + + it('shows a validation error when threads is negative', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'threads', -1) + + expect((wrapper.vm as any).trapConfigError.threads).toBe('Threads cannot be negative.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('shows a validation error when queue size is negative', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'queueSize', -1) + + expect((wrapper.vm as any).trapConfigError.queueSize).toBe('Queue Size cannot be negative.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('shows a validation error when batch size is negative', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'batchSize', -1) + + expect((wrapper.vm as any).trapConfigError.batchSize).toBe('Batch Size cannot be negative.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('shows a validation error when batch interval is negative', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'batchInterval', -1) + + expect((wrapper.vm as any).trapConfigError.batchInterval).toBe('Batch Interval cannot be negative.') + expect((wrapper.vm as any).isSaveDisabled).toBe(true) + }) + + it('does not call the update service when the form is invalid', async () => { + const wrapper = mountComponent() + + await setBindingValue(wrapper, 'bindAddress', '') + await (wrapper.vm as any).updateConfig() + await flushPromises() + + expect(updateTrapdConfigurationMock).not.toHaveBeenCalled() + expect(showSnackBarMock).not.toHaveBeenCalled() + }) +}) \ No newline at end of file diff --git a/ui/tests/components/TrapConfiguration/SnmpV3UserManagement.test.ts b/ui/tests/components/TrapConfiguration/SnmpV3UserManagement.test.ts new file mode 100644 index 000000000000..4c96f31d029f --- /dev/null +++ b/ui/tests/components/TrapConfiguration/SnmpV3UserManagement.test.ts @@ -0,0 +1,360 @@ +import SnmpV3UserManagement from '@/components/TrapConfiguration/SnmpV3UserManagement.vue' +import { deleteTrapdUser } from '@/services/trapdConfigurationService' +import { useTrapConfigStore } from '@/stores/trapConfigStore' +import { CreateEditMode } from '@/types' +import type { SnmpV3User } from '@/types/trapConfig' +import { FeatherSortHeader, SORT } from '@featherds/table' +import { createTestingPinia } from '@pinia/testing' +import { flushPromises, mount } from '@vue/test-utils' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { defineComponent, nextTick } from 'vue' + +const { showSnackBarMock } = vi.hoisted(() => ({ + showSnackBarMock: vi.fn() +})) + +vi.mock('@/composables/useSnackbar', () => ({ + default: () => ({ + showSnackBar: showSnackBarMock + }) +})) + +vi.mock('@/services/trapdConfigurationService', () => ({ + deleteTrapdUser: vi.fn() +})) + +const FeatherButtonStub = defineComponent({ + name: 'FeatherButton', + props: { + dataTest: { + type: String, + default: '' + }, + disabled: { + type: Boolean, + default: false + }, + icon: { + type: String, + default: '' + } + }, + emits: ['click'], + template: + '' +}) + +const FeatherSortHeaderStub = defineComponent({ + name: 'FeatherSortHeader', + props: { + property: { + type: String, + default: '' + }, + sort: { + type: String, + default: '' + } + }, + emits: ['sort-changed'], + template: '' +}) + +const DeleteDialogStub = defineComponent({ + name: 'DeleteUserConfirmationDialog', + props: { + visible: { + type: Boolean, + default: false + } + }, + emits: ['close', 'confirm'], + template: ` +
+
{{ String(visible) }}
+ + +
+ ` +}) + +describe('SnmpV3UserManagement.vue', () => { + let store: ReturnType + const deleteTrapdUserMock = vi.mocked(deleteTrapdUser) + + const users: SnmpV3User[] = [ + { + engineId: null, + securityName: 'user-one', + securityLevel: 1, + authProtocol: null, + authPassphrase: null, + privacyProtocol: null, + privacyPassphrase: null + }, + { + engineId: null, + securityName: 'user-two', + securityLevel: 3, + authProtocol: 'SHA256', + authPassphrase: 'masked-a', + privacyProtocol: 'AES256', + privacyPassphrase: 'masked-b' + } + ] + + const mountComponent = () => { + return mount(SnmpV3UserManagement, { + global: { + stubs: { + TableCard: { + template: '
' + }, + EmptyList: { + props: ['content'], + template: '
{{ content.msg }}
' + }, + DeleteUserConfirmationDialog: DeleteDialogStub, + FeatherButton: FeatherButtonStub, + 'feather-button': FeatherButtonStub, + FeatherSortHeader: FeatherSortHeaderStub, + 'feather-sort-header': FeatherSortHeaderStub, + FeatherIcon: true, + 'feather-icon': true, + TransitionGroup: { + template: '' + } + } + } + }) + } + + const clickByDataTest = async (wrapper: ReturnType, dataTest: string, index = 0) => { + const elements = wrapper.findAll(`[data-test="${dataTest}"]`) + expect(elements[index]?.exists()).toBe(true) + await elements[index].trigger('click') + await flushPromises() + } + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia( + createTestingPinia({ + stubActions: false, + createSpy: vi.fn + }) + ) + + store = useTrapConfigStore() + store.createUserDrawerState.visible = false + store.SnmpV3Users = [...users] + store.fetchTrapConfig = vi.fn().mockResolvedValue(undefined) + store.openCreateUserDrawer = vi.fn() + + deleteTrapdUserMock.mockResolvedValue(undefined) + }) + + it('does not render when the create user drawer is visible', () => { + store.createUserDrawerState.visible = true + const wrapper = mountComponent() + + expect(wrapper.find('[data-test="snmpv3-user-management"]').exists()).toBe(false) + }) + + it('renders the heading, add button, column headers, and user rows', () => { + const wrapper = mountComponent() + + expect(wrapper.text()).toContain('SNMPv3 User Management') + expect(wrapper.text()).toContain('List SNMPv3 users credentials') + expect(wrapper.find('[data-test="add-user-button"]').exists()).toBe(true) + expect(wrapper.text()).toContain('SnmpV3 Username') + expect(wrapper.text()).toContain('Security Level') + expect(wrapper.text()).toContain('Authentication Protocol') + expect(wrapper.text()).toContain('Privacy Protocol') + expect(wrapper.text()).toContain('Action') + expect(wrapper.text()).toContain('user-one') + expect(wrapper.text()).toContain('user-two') + expect((wrapper.vm as any).tableRecords).toEqual(users) + }) + + it('renders the empty state when there are no users', async () => { + store.SnmpV3Users = [] + const wrapper = mountComponent() + await nextTick() + + expect(wrapper.find('[data-test="empty-list"]').text()).toBe('No SNMPv3 users found') + expect(wrapper.findAll('tbody tr')).toHaveLength(0) + }) + + it('opens create user drawer in create mode from the add user button', async () => { + const wrapper = mountComponent() + + await clickByDataTest(wrapper, 'add-user-button') + + expect(store.openCreateUserDrawer).toHaveBeenCalledWith(CreateEditMode.Create, -1) + }) + + it('opens create user drawer in edit mode for the selected row', async () => { + const wrapper = mountComponent() + + await clickByDataTest(wrapper, 'edit-user-button', 1) + + expect(store.openCreateUserDrawer).toHaveBeenCalledWith(CreateEditMode.Edit, 1) + }) + + it('opens the delete dialog with the selected security name', async () => { + const wrapper = mountComponent() + + await clickByDataTest(wrapper, 'delete-user-button', 1) + + expect((wrapper.vm as any).deleteUserSecurityName).toBe('user-two') + expect((wrapper.vm as any).deleteDialogVisible).toBe(true) + expect(wrapper.find('[data-test="delete-dialog-visible"]').text()).toBe('true') + }) + + it('cancels delete and resets dialog state', async () => { + const wrapper = mountComponent() + ;(wrapper.vm as any).openDeleteUserDialog('user-one') + await nextTick() + + await clickByDataTest(wrapper, 'close-delete-dialog') + + expect((wrapper.vm as any).deleteUserSecurityName).toBe(null) + expect((wrapper.vm as any).deleteDialogVisible).toBe(false) + }) + + it('deletes the selected user successfully, refreshes config, and closes the dialog', async () => { + const wrapper = mountComponent() + ;(wrapper.vm as any).openDeleteUserDialog('user-one') + await nextTick() + + await clickByDataTest(wrapper, 'confirm-delete-dialog') + + expect(deleteTrapdUserMock).toHaveBeenCalledWith('user-one') + expect(store.fetchTrapConfig).toHaveBeenCalledTimes(1) + expect((wrapper.vm as any).deleteUserSecurityName).toBe(null) + expect((wrapper.vm as any).deleteDialogVisible).toBe(false) + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'SNMPv3 user deleted successfully.' }) + expect((wrapper.vm as any).isDeleting).toBe(false) + }) + + it('shows the service error when delete fails with an Error', async () => { + deleteTrapdUserMock.mockRejectedValue(new Error('delete failed')) + const wrapper = mountComponent() + ;(wrapper.vm as any).openDeleteUserDialog('user-one') + await nextTick() + + await clickByDataTest(wrapper, 'confirm-delete-dialog') + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'delete failed', error: true }) + expect(store.fetchTrapConfig).not.toHaveBeenCalled() + expect((wrapper.vm as any).deleteUserSecurityName).toBe('user-one') + expect((wrapper.vm as any).deleteDialogVisible).toBe(true) + expect((wrapper.vm as any).isDeleting).toBe(false) + }) + + it('shows a generic error when delete fails with a non-Error value', async () => { + deleteTrapdUserMock.mockRejectedValue('boom') + const wrapper = mountComponent() + ;(wrapper.vm as any).openDeleteUserDialog('user-one') + await nextTick() + + await clickByDataTest(wrapper, 'confirm-delete-dialog') + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to delete SNMPv3 user.', error: true }) + }) + + it('does not call delete when no selected security name exists', async () => { + const wrapper = mountComponent() + + await (wrapper.vm as any).confirmDeleteUser() + await flushPromises() + + expect(deleteTrapdUserMock).not.toHaveBeenCalled() + expect(showSnackBarMock).not.toHaveBeenCalled() + }) + + it('does not call delete again while a delete request is already in progress', async () => { + let resolveDelete: (() => void) | undefined + deleteTrapdUserMock.mockImplementation( + () => + new Promise((resolve) => { + resolveDelete = resolve + }) + ) + + const wrapper = mountComponent() + ;(wrapper.vm as any).openDeleteUserDialog('user-one') + await nextTick() + + const pendingDelete = (wrapper.vm as any).confirmDeleteUser() + await nextTick() + await (wrapper.vm as any).confirmDeleteUser() + + expect(deleteTrapdUserMock).toHaveBeenCalledTimes(1) + expect((wrapper.vm as any).isDeleting).toBe(true) + + resolveDelete?.() + await pendingDelete + await flushPromises() + + expect((wrapper.vm as any).isDeleting).toBe(false) + }) + + it('updates tableRecords when the store users list changes', async () => { + const wrapper = mountComponent() + const nextUsers: SnmpV3User[] = [ + { + engineId: null, + securityName: 'replacement-user', + securityLevel: 2, + authProtocol: 'MD5', + authPassphrase: 'masked', + privacyProtocol: null, + privacyPassphrase: null + } + ] + + store.SnmpV3Users = nextUsers + await nextTick() + await nextTick() + + expect((wrapper.vm as any).tableRecords).toEqual(nextUsers) + expect(wrapper.text()).toContain('replacement-user') + expect(wrapper.text()).not.toContain('user-one') + }) + + it('updates sort state when a sort header emits sort-changed', async () => { + const wrapper = mountComponent() + const sortHeaders = wrapper.findAllComponents(FeatherSortHeader) + + expect(sortHeaders).toHaveLength(4) + await sortHeaders[1].vm.$emit('sort-changed', { + property: 'securityLevel', + value: SORT.ASCENDING + }) + await nextTick() + + expect((wrapper.vm as any).sort.username).toBe(SORT.NONE) + expect((wrapper.vm as any).sort.securityLevel).toBe(SORT.ASCENDING) + expect((wrapper.vm as any).sort.authenticationProtocol).toBe(SORT.NONE) + expect((wrapper.vm as any).sort.privacyProtocol).toBe(SORT.NONE) + }) + + it('sortChanged resets previous sort values before applying the next property', () => { + const wrapper = mountComponent() + + ;(wrapper.vm as any).sort.username = SORT.DESCENDING + ;(wrapper.vm as any).sort.securityLevel = SORT.ASCENDING + ;(wrapper.vm as any).sortChanged({ + property: 'privacyProtocol', + value: SORT.ASCENDING + }) + + expect((wrapper.vm as any).sort.username).toBe(SORT.NONE) + expect((wrapper.vm as any).sort.securityLevel).toBe(SORT.NONE) + expect((wrapper.vm as any).sort.authenticationProtocol).toBe(SORT.NONE) + expect((wrapper.vm as any).sort.privacyProtocol).toBe(SORT.ASCENDING) + }) +}) diff --git a/ui/tests/containers/TrapConfiguration.test.ts b/ui/tests/containers/TrapConfiguration.test.ts new file mode 100644 index 000000000000..aeba2b9e7428 --- /dev/null +++ b/ui/tests/containers/TrapConfiguration.test.ts @@ -0,0 +1,252 @@ +import BreadCrumbs from '@/components/Layout/BreadCrumbs.vue' +import TrapConfiguration from '@/containers/TrapConfiguration.vue' +import { validateTrapdXml } from '@/lib/trapdValidator' +import { uploadTrapdConfiguration } from '@/services/trapdConfigurationService' +import { useMenuStore } from '@/stores/menuStore' +import { useTrapConfigStore } from '@/stores/trapConfigStore' +import { createTestingPinia } from '@pinia/testing' +import { mount } from '@vue/test-utils' +import { setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { showSnackBarMock } = vi.hoisted(() => ({ + showSnackBarMock: vi.fn() +})) + +vi.mock('@/composables/useSnackbar', () => ({ + default: () => ({ + showSnackBar: showSnackBarMock + }) +})) + +vi.mock('@/lib/trapdValidator', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateTrapdXml: vi.fn() + } +}) + +vi.mock('@/services/trapdConfigurationService', () => ({ + uploadTrapdConfiguration: vi.fn() +})) + +describe('TrapConfiguration.vue', () => { + let trapStore: ReturnType + let menuStore: ReturnType + + const validateTrapdXmlMock = vi.mocked(validateTrapdXml) + const uploadTrapdConfigurationMock = vi.mocked(uploadTrapdConfiguration) + + const mountComponent = () => { + return mount(TrapConfiguration, { + global: { + stubs: { + GeneralConfiguration: true, + SnmpV3UserManagement: true, + CreateSnmpV3User: true, + FeatherTabContainer: { + template: '
' + }, + FeatherTab: { + template: '
' + }, + FeatherTabPanel: { + template: '
' + }, + FeatherButton: { + template: '' + }, + BreadCrumbs: true + } + } + }) + } + + const createXmlFile = (name = 'trapd.xml', content = '') => { + const file = new File([content], name, { type: 'text/xml' }) + vi.spyOn(file, 'text').mockResolvedValue(content) + return file + } + + const triggerUpload = async (wrapper: ReturnType, file?: File) => { + const input = wrapper.find('input[type="file"]') + Object.defineProperty(input.element, 'files', { + value: file ? [file] : [], + configurable: true + }) + await input.trigger('change') + } + + beforeEach(() => { + vi.clearAllMocks() + setActivePinia(createTestingPinia({ stubActions: true })) + trapStore = useTrapConfigStore() + menuStore = useMenuStore() + menuStore.mainMenu = { homeUrl: '/home' } as any + trapStore.fetchTrapConfig = vi.fn().mockResolvedValue(undefined) + validateTrapdXmlMock.mockReturnValue({ valid: true, errors: [] }) + uploadTrapdConfigurationMock.mockResolvedValue(undefined) + }) + + it('renders heading and child sections', () => { + const wrapper = mountComponent() + + expect(wrapper.find('h1').text()).toBe('TrapD Configuration') + expect(wrapper.findComponent(BreadCrumbs).exists()).toBe(true) + expect(wrapper.find('input[type="file"]').exists()).toBe(true) + }) + + it('renders breadcrumbs with home and trap configuration entries', () => { + const wrapper = mountComponent() + const breadcrumbs = wrapper.findComponent(BreadCrumbs) + const items = breadcrumbs.props('items') + + expect(items).toHaveLength(2) + expect(items[0]).toEqual({ label: 'Home', to: '/home', isAbsoluteLink: true }) + expect(items[1]).toEqual({ label: 'Trap Configurations', to: '#', position: 'last' }) + }) + + it('calls fetchTrapConfig on mount', () => { + mountComponent() + expect(trapStore.fetchTrapConfig).toHaveBeenCalledTimes(1) + }) + + it('shows snackbar when initial fetch fails with Error', async () => { + trapStore.fetchTrapConfig = vi.fn().mockRejectedValue(new Error('initial fetch failed')) + + mountComponent() + await Promise.resolve() + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'initial fetch failed', error: true }) + }) + + it('shows snackbar when initial fetch fails with non-Error', async () => { + trapStore.fetchTrapConfig = vi.fn().mockRejectedValue('boom') + + mountComponent() + await Promise.resolve() + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to retrieve trapd configuration.', error: true }) + }) + + it('opens file chooser when upload button is clicked', async () => { + const wrapper = mountComponent() + const input = wrapper.find('input[type="file"]').element as HTMLInputElement + const clickSpy = vi.spyOn(input, 'click').mockImplementation(() => undefined) + + const uploadButton = wrapper.findAll('button').find((button) => button.text().includes('Upload Configuration')) + expect(uploadButton).toBeDefined() + + await uploadButton!.trigger('click') + expect(clickSpy).toHaveBeenCalledTimes(1) + }) + + it('returns early when no file is selected', async () => { + const wrapper = mountComponent() + await triggerUpload(wrapper) + + expect(validateTrapdXmlMock).not.toHaveBeenCalled() + expect(uploadTrapdConfigurationMock).not.toHaveBeenCalled() + }) + + it('rejects non-xml files before validation', async () => { + const wrapper = mountComponent() + const invalidFile = createXmlFile('trapd.txt', 'not xml') + + await triggerUpload(wrapper, invalidFile) + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Only .xml files are supported.', error: true }) + expect(validateTrapdXmlMock).not.toHaveBeenCalled() + expect(uploadTrapdConfigurationMock).not.toHaveBeenCalled() + }) + + it('shows read error when file.text fails', async () => { + const wrapper = mountComponent() + const file = new File(['x'], 'trapd.xml', { type: 'text/xml' }) + vi.spyOn(file, 'text').mockRejectedValue(new Error('read failed')) + + await triggerUpload(wrapper, file) + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to read XML file.', error: true }) + expect(validateTrapdXmlMock).not.toHaveBeenCalled() + expect(uploadTrapdConfigurationMock).not.toHaveBeenCalled() + }) + + it('blocks upload and shows validation errors when XML is invalid (<=3 errors)', async () => { + const wrapper = mountComponent() + const file = createXmlFile('trapd.xml', '') + validateTrapdXmlMock.mockReturnValue({ + valid: false, + errors: [ + { field: 'root', message: 'Root mismatch' }, + { field: 'xmlns', message: 'Namespace invalid' }, + { field: 'snmp-trap-port', message: 'Port invalid' } + ] + }) + + await triggerUpload(wrapper, file) + + expect(validateTrapdXmlMock).toHaveBeenCalledWith('') + expect(showSnackBarMock).toHaveBeenCalledWith({ + msg: 'Invalid trap configuration XML: Root mismatch | Namespace invalid | Port invalid', + error: true + }) + expect(uploadTrapdConfigurationMock).not.toHaveBeenCalled() + }) + + it('shows +N more suffix for validation errors beyond first 3', async () => { + const wrapper = mountComponent() + const file = createXmlFile('trapd.xml', '') + validateTrapdXmlMock.mockReturnValue({ + valid: false, + errors: [ + { field: 'a', message: 'e1' }, + { field: 'b', message: 'e2' }, + { field: 'c', message: 'e3' }, + { field: 'd', message: 'e4' }, + { field: 'e', message: 'e5' } + ] + }) + + await triggerUpload(wrapper, file) + + expect(showSnackBarMock).toHaveBeenCalledWith({ + msg: 'Invalid trap configuration XML: e1 | e2 | e3 (+2 more)', + error: true + }) + expect(uploadTrapdConfigurationMock).not.toHaveBeenCalled() + }) + + it('uploads file and refreshes store when XML is valid', async () => { + const wrapper = mountComponent() + const file = createXmlFile('trapd.xml', '') + + await triggerUpload(wrapper, file) + + expect(validateTrapdXmlMock).toHaveBeenCalledWith('') + expect(uploadTrapdConfigurationMock).toHaveBeenCalledWith(file) + expect(trapStore.fetchTrapConfig).toHaveBeenCalledTimes(2) + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Trap configuration uploaded successfully.' }) + }) + + it('shows upload error message from Error instance', async () => { + const wrapper = mountComponent() + const file = createXmlFile('trapd.xml', '') + uploadTrapdConfigurationMock.mockRejectedValue(new Error('upload failed')) + + await triggerUpload(wrapper, file) + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'upload failed', error: true }) + }) + + it('shows generic upload error message for non-Error throw', async () => { + const wrapper = mountComponent() + const file = createXmlFile('trapd.xml', '') + uploadTrapdConfigurationMock.mockRejectedValue('bad') + + await triggerUpload(wrapper, file) + + expect(showSnackBarMock).toHaveBeenCalledWith({ msg: 'Failed to upload trap configuration.', error: true }) + }) +}) diff --git a/ui/tests/lib/trapdValidator.test.ts b/ui/tests/lib/trapdValidator.test.ts new file mode 100644 index 000000000000..a12557e3e364 --- /dev/null +++ b/ui/tests/lib/trapdValidator.test.ts @@ -0,0 +1,693 @@ +import { XMLParser, XMLValidator } from 'fast-xml-parser' +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest' +import { + AUTH_PROTOCOL_OPTIONS, + AuthProtocol, + AuthProtocols, + MAX_PORT, + MIN_PORT, + PRIVACY_PROTOCOL_OPTIONS, + PrivacyProtocol, + PrivacyProtocols, + SECURITY_LEVEL_OPTIONS, + SecurityLevel, + TRAPD_XML_NAMESPACE, + getDefaultTrapdConfig, + isValidIP, + isValidPort, + isValidSnmpSecurityLevel, + validateTrapdXml +} from '@/lib/trapdValidator' + +// --------------------------------------------------------------------------- +// DOMParser polyfill (fast-xml-parser backed) +// +// happy-dom v9 parses ALL MIME types as HTML, so root.namespaceURI is always +// the XHTML namespace. We replace window.DOMParser with a minimal but correct +// implementation for the subset of the DOM API that validateTrapdXml uses. +// --------------------------------------------------------------------------- + +class FakeElement { + localName: string + namespaceURI: string | null + textContent: string | null = null + private attrs: Record + private children: FakeElement[] + + constructor(localName: string, attrs: Record, children: FakeElement[] = []) { + this.localName = localName + this.attrs = attrs + this.children = children + this.namespaceURI = attrs['xmlns'] ?? null + } + + getAttribute(name: string): string | null { + return Object.prototype.hasOwnProperty.call(this.attrs, name) ? this.attrs[name] : null + } + + getElementsByTagName(tagName: string): FakeElement[] { + const results: FakeElement[] = [] + for (const child of this.children) { + if (child.localName === tagName) results.push(child) + results.push(...child.getElementsByTagName(tagName)) + } + return results + } +} + +class FakeDocument { + documentElement: FakeElement + private parseError: FakeElement | null + + constructor(root: FakeElement, parseError: FakeElement | null = null) { + this.documentElement = root + this.parseError = parseError + } + + querySelector(selector: string): FakeElement | null { + return selector === 'parsererror' ? this.parseError : null + } +} + +function buildElement(tagName: string, node: Record): FakeElement { + const attrs: Record = {} + const children: FakeElement[] = [] + + for (const [key, value] of Object.entries(node)) { + if (key.startsWith('@_')) { + attrs[key.slice(2)] = String(value) + } else if (Array.isArray(value)) { + for (const child of value as Record[]) { + children.push(buildElement(key, child)) + } + } else if (typeof value === 'object' && value !== null) { + children.push(buildElement(key, value as Record)) + } + } + + return new FakeElement(tagName, attrs, children) +} + +const fxpParser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + isArray: (name) => name === 'snmpv3-user', + parseAttributeValue: false, + trimValues: false +}) + +class FakeDOMParser { + parseFromString(xmlString: string, _mimeType: string): FakeDocument { + void _mimeType + const validation = XMLValidator.validate(xmlString) + if (validation !== true) { + const errEl = new FakeElement('parsererror', {}) + errEl.textContent = (validation as { err: { msg: string } }).err?.msg ?? 'parse error' + const dummyRoot = new FakeElement('parsererror', {}) + return new FakeDocument(dummyRoot, errEl) + } + + const parsed = fxpParser.parse(xmlString) as Record> + const rootTagName = Object.keys(parsed)[0] + const rootNode = parsed[rootTagName] ?? {} + const root = buildElement(rootTagName, rootNode) + return new FakeDocument(root) + } +} + +beforeAll(() => { + vi.stubGlobal('DOMParser', FakeDOMParser) +}) + +afterAll(() => { + vi.unstubAllGlobals() +}) + +const VALID_NS = TRAPD_XML_NAMESPACE + +/** Build a minimal valid trapd XML string, overriding individual attributes. */ +const buildXml = (overrides: { + root?: string + xmlns?: string | null + address?: string | null + port?: string | null + suspect?: string | null + users?: string +} = {}): string => { + const { + root = 'trapd-configuration', + xmlns = VALID_NS, + address = '*', + port = '162', + suspect = null, + users = '' + } = overrides + + const nsAttr = xmlns === null ? '' : ` xmlns="${xmlns}"` + const addrAttr = address === null ? '' : ` snmp-trap-address="${address}"` + const portAttr = port === null ? '' : ` snmp-trap-port="${port}"` + const suspectAttr = suspect === null ? '' : ` new-suspect-on-trap="${suspect}"` + + if (users) { + return `<${root}${nsAttr}${addrAttr}${portAttr}${suspectAttr}>${users}` + } + return `<${root}${nsAttr}${addrAttr}${portAttr}${suspectAttr} />` +} + +/** Build a element string from an attribute map. */ +const buildUser = (attrs: Record = {}): string => { + const attrStr = Object.entries(attrs) + .map(([k, v]) => `${k}="${v}"`) + .join(' ') + return `` +} + +describe('isValidPort', () => { + it.each([MIN_PORT, MAX_PORT, 162, 10162, 1024])('returns true for valid port %i', (port) => { + expect(isValidPort(port)).toBe(true) + }) + + it('returns false for undefined', () => { + expect(isValidPort(undefined)).toBe(false) + }) + + it('returns false for 0 (below MIN_PORT)', () => { + expect(isValidPort(0)).toBe(false) + }) + + it('returns false for MAX_PORT + 1', () => { + expect(isValidPort(MAX_PORT + 1)).toBe(false) + }) + + it('returns false for negative port', () => { + expect(isValidPort(-1)).toBe(false) + }) + + it('returns false for NaN', () => { + expect(isValidPort(NaN)).toBe(false) + }) +}) + +describe('isValidIP', () => { + it.each(['0.0.0.0', '192.168.1.1', '255.255.255.255', '10.0.0.1'])( + 'returns true for valid IPv4 %s', + (ip) => { + expect(isValidIP(ip)).toBe(true) + } + ) + + it('returns false for too few octets', () => { + expect(isValidIP('192.168.1')).toBe(false) + }) + + it('returns false for too many octets', () => { + expect(isValidIP('192.168.1.1.1')).toBe(false) + }) + + it('returns false for octet > 255', () => { + expect(isValidIP('192.168.1.256')).toBe(false) + }) + + it('returns false for non-numeric octet', () => { + expect(isValidIP('abc.def.ghi.jkl')).toBe(false) + }) + + it('returns false for empty string', () => { + expect(isValidIP('')).toBe(false) + }) + + it('returns false for wildcard *', () => { + expect(isValidIP('*')).toBe(false) + }) +}) + +describe('isValidSnmpSecurityLevel', () => { + it.each([SecurityLevel.NoAuthNoPriv, SecurityLevel.AuthNoPriv, SecurityLevel.AuthPriv])( + 'returns true for valid level %i', + (level) => { + expect(isValidSnmpSecurityLevel(level)).toBe(true) + } + ) + + it('returns false for SecurityLevel.None (0)', () => { + expect(isValidSnmpSecurityLevel(SecurityLevel.None)).toBe(false) + }) + + it('returns false for level 4 (out of range)', () => { + expect(isValidSnmpSecurityLevel(4)).toBe(false) + }) + + it('returns false for undefined', () => { + expect(isValidSnmpSecurityLevel(undefined)).toBe(false) + }) +}) + +describe('getDefaultTrapdConfig', () => { + it('returns expected default values', () => { + const config = getDefaultTrapdConfig() + expect(config.snmpTrapAddress).toBe('*') + expect(config.snmpTrapPort).toBe(10162) + expect(config.newSuspectOnTrap).toBe(false) + expect(config.includeRawMessage).toBe(false) + expect(config.threads).toBe(0) + expect(config.queueSize).toBe(10000) + expect(config.batchSize).toBe(1000) + expect(config.batchInterval).toBe(500) + expect(config.useAddressFromVarbind).toBe(false) + expect(config.snmpv3User).toEqual([]) + }) + + it('returns a fresh object each call (no shared reference)', () => { + const a = getDefaultTrapdConfig() + const b = getDefaultTrapdConfig() + a.snmpv3User.push({ securityName: 'x' } as any) + expect(b.snmpv3User).toHaveLength(0) + }) +}) + +describe('SECURITY_LEVEL_OPTIONS', () => { + it('contains exactly three entries', () => { + expect(SECURITY_LEVEL_OPTIONS).toHaveLength(3) + }) + + it('has correct _value strings for all three levels', () => { + const values = SECURITY_LEVEL_OPTIONS.map((o) => o._value) + expect(values).toContain(String(SecurityLevel.NoAuthNoPriv)) + expect(values).toContain(String(SecurityLevel.AuthNoPriv)) + expect(values).toContain(String(SecurityLevel.AuthPriv)) + }) +}) + +describe('AUTH_PROTOCOL_OPTIONS', () => { + it('contains exactly as many entries as AuthProtocols', () => { + expect(AUTH_PROTOCOL_OPTIONS).toHaveLength(AuthProtocols.length) + }) + + it('maps protocol values correctly', () => { + const values = AUTH_PROTOCOL_OPTIONS.map((o) => o._value) + expect(values).toContain(AuthProtocol.MD5) + expect(values).toContain(AuthProtocol.SHA) + expect(values).toContain(AuthProtocol.SHA256) + expect(values).toContain(AuthProtocol.SHA512) + }) +}) + +describe('PRIVACY_PROTOCOL_OPTIONS', () => { + it('contains exactly as many entries as PrivacyProtocols', () => { + expect(PRIVACY_PROTOCOL_OPTIONS).toHaveLength(PrivacyProtocols.length) + }) + + it('maps protocol values correctly', () => { + const values = PRIVACY_PROTOCOL_OPTIONS.map((o) => o._value) + expect(values).toContain(PrivacyProtocol.DES) + expect(values).toContain(PrivacyProtocol.AES) + expect(values).toContain(PrivacyProtocol.AES256) + }) +}) + +describe('validateTrapdXml – XML structure', () => { + it('returns invalid for empty string', () => { + const result = validateTrapdXml('') + expect(result.valid).toBe(false) + expect(result.errors[0].field).toBe('xml') + }) + + it('returns invalid for whitespace-only string', () => { + const result = validateTrapdXml(' \n ') + expect(result.valid).toBe(false) + expect(result.errors[0].field).toBe('xml') + }) + + it('returns invalid for malformed XML', () => { + const result = validateTrapdXml(' { + const result = validateTrapdXml(``) + expect(result.valid).toBe(false) + expect(result.errors[0].field).toBe('root') + expect(result.errors[0].message).toMatch(/trapd-configuration/) + }) + + it('returns invalid for wrong xmlns', () => { + const result = validateTrapdXml(buildXml({ xmlns: 'http://wrong.namespace' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'xmlns')).toBe(true) + }) + + it('returns invalid when xmlns is omitted', () => { + const result = validateTrapdXml(buildXml({ xmlns: null })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'xmlns')).toBe(true) + }) + + it('returns valid for minimal correct XML', () => { + const result = validateTrapdXml(buildXml()) + expect(result.valid).toBe(true) + expect(result.errors).toHaveLength(0) + }) +}) + +describe('validateTrapdXml – snmp-trap-address', () => { + it('returns error when snmp-trap-address is missing', () => { + const result = validateTrapdXml(buildXml({ address: null })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-address')).toBe(true) + }) + + it('accepts wildcard "*"', () => { + const result = validateTrapdXml(buildXml({ address: '*' })) + expect(result.valid).toBe(true) + }) + + it('accepts a valid IPv4 address', () => { + const result = validateTrapdXml(buildXml({ address: '192.168.1.1' })) + expect(result.valid).toBe(true) + }) + + it('returns error for invalid IPv4 address', () => { + const result = validateTrapdXml(buildXml({ address: '999.0.0.1' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-address')).toBe(true) + }) + + it('returns error for hostname (not IPv4)', () => { + const result = validateTrapdXml(buildXml({ address: 'localhost' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-address')).toBe(true) + }) +}) + +describe('validateTrapdXml – snmp-trap-port', () => { + it('returns error when snmp-trap-port is missing', () => { + const result = validateTrapdXml(buildXml({ port: null })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-port')).toBe(true) + }) + + it('accepts MIN_PORT boundary', () => { + const result = validateTrapdXml(buildXml({ port: String(MIN_PORT) })) + expect(result.valid).toBe(true) + }) + + it('accepts MAX_PORT boundary', () => { + const result = validateTrapdXml(buildXml({ port: String(MAX_PORT) })) + expect(result.valid).toBe(true) + }) + + it('returns error for port 0 (below MIN_PORT)', () => { + const result = validateTrapdXml(buildXml({ port: '0' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-port')).toBe(true) + }) + + it('returns error for port 65536 (above MAX_PORT)', () => { + const result = validateTrapdXml(buildXml({ port: '65536' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-port')).toBe(true) + }) + + it('returns error for non-numeric port', () => { + const result = validateTrapdXml(buildXml({ port: 'abc' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'snmp-trap-port')).toBe(true) + }) +}) + +describe('validateTrapdXml – new-suspect-on-trap', () => { + it('accepts "true"', () => { + const result = validateTrapdXml(buildXml({ suspect: 'true' })) + expect(result.valid).toBe(true) + }) + + it('accepts "false"', () => { + const result = validateTrapdXml(buildXml({ suspect: 'false' })) + expect(result.valid).toBe(true) + }) + + it('is optional (absent is valid)', () => { + const result = validateTrapdXml(buildXml({ suspect: null })) + expect(result.valid).toBe(true) + }) + + it('returns error for invalid value', () => { + const result = validateTrapdXml(buildXml({ suspect: 'yes' })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field === 'new-suspect-on-trap')).toBe(true) + }) +}) + +describe('validateTrapdXml – snmpv3-user: security-name and security-level', () => { + it('returns error when security-name is missing', () => { + const user = buildUser({ 'security-level': '1' }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('security-name'))).toBe(true) + }) + + it('returns error when security-level is missing', () => { + const user = buildUser({ 'security-name': 'user1' }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('security-level'))).toBe(true) + }) + + it('returns error for security-level 0 (None)', () => { + const user = buildUser({ 'security-name': 'user1', 'security-level': '0' }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('security-level'))).toBe(true) + }) + + it('returns error for security-level 4 (out of range)', () => { + const user = buildUser({ 'security-name': 'user1', 'security-level': '4' }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('security-level'))).toBe(true) + }) +}) + +describe('validateTrapdXml – snmpv3-user: level 1 (NoAuthNoPriv)', () => { + it('is valid with only security-name and security-level=1', () => { + const user = buildUser({ 'security-name': 'user1', 'security-level': '1' }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.valid).toBe(true) + }) + + it('returns error when auth-protocol is present at level 1', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '1', + 'auth-protocol': 'MD5', + 'auth-passphrase': 'pass' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(true) + }) + + it('returns error when auth-passphrase is present at level 1', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '1', + 'auth-passphrase': 'pass' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-passphrase'))).toBe(true) + }) + + it('returns error when privacy-protocol is present at level 1', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '1', + 'privacy-protocol': 'DES', + 'privacy-passphrase': 'priv' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('privacy-protocol'))).toBe(true) + }) + + it('returns error when privacy-passphrase is present at level 1', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '1', + 'privacy-passphrase': 'priv' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('privacy-passphrase'))).toBe(true) + }) +}) + +describe('validateTrapdXml – snmpv3-user: level 2 (AuthNoPriv)', () => { + it('is valid with auth-protocol and auth-passphrase at level 2', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'SHA', + 'auth-passphrase': 'secret' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.valid).toBe(true) + }) + + it('returns error when auth-protocol is missing at level 2', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-passphrase': 'secret' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(true) + }) + + it('returns error when auth-passphrase is missing at level 2', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'MD5' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-passphrase'))).toBe(true) + }) + + it('returns error when privacy-protocol is present at level 2', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'MD5', + 'auth-passphrase': 'secret', + 'privacy-protocol': 'DES', + 'privacy-passphrase': 'priv' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('privacy-protocol'))).toBe(true) + }) + + it('returns error when privacy-passphrase is present at level 2', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'MD5', + 'auth-passphrase': 'secret', + 'privacy-passphrase': 'priv' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('privacy-passphrase'))).toBe(true) + }) +}) + +describe('validateTrapdXml – snmpv3-user: level 3 (AuthPriv)', () => { + const validLevel3 = { + 'security-name': 'user1', + 'security-level': '3', + 'auth-protocol': 'SHA', + 'auth-passphrase': 'authsecret', + 'privacy-protocol': 'AES', + 'privacy-passphrase': 'privsecret' + } + + it('is valid with all required fields at level 3', () => { + const result = validateTrapdXml(buildXml({ users: buildUser(validLevel3) })) + expect(result.valid).toBe(true) + }) + + it('returns error when auth-protocol is missing at level 3', () => { + const { 'auth-protocol': omittedAuthProtocol, ...attrs } = validLevel3 + void omittedAuthProtocol + const result = validateTrapdXml(buildXml({ users: buildUser(attrs) })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(true) + }) + + it('returns error when auth-passphrase is missing at level 3', () => { + const { 'auth-passphrase': omittedAuthPassphrase, ...attrs } = validLevel3 + void omittedAuthPassphrase + const result = validateTrapdXml(buildXml({ users: buildUser(attrs) })) + expect(result.errors.some((e) => e.field.includes('auth-passphrase'))).toBe(true) + }) + + it('returns error when privacy-protocol is missing at level 3', () => { + const { 'privacy-protocol': omittedPrivacyProtocol, ...attrs } = validLevel3 + void omittedPrivacyProtocol + const result = validateTrapdXml(buildXml({ users: buildUser(attrs) })) + expect(result.errors.some((e) => e.field.includes('privacy-protocol'))).toBe(true) + }) + + it('returns error when privacy-passphrase is missing at level 3', () => { + const { 'privacy-passphrase': omittedPrivacyPassphrase, ...attrs } = validLevel3 + void omittedPrivacyPassphrase + const result = validateTrapdXml(buildXml({ users: buildUser(attrs) })) + expect(result.errors.some((e) => e.field.includes('privacy-passphrase'))).toBe(true) + }) +}) + +describe('validateTrapdXml – auth-protocol dash normalization', () => { + it('accepts "SHA-256" (dash form) as valid auth-protocol', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'SHA-256', + 'auth-passphrase': 'secret' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(false) + }) + + it('rejects completely unknown auth-protocol value', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '2', + 'auth-protocol': 'UNKNOWN', + 'auth-passphrase': 'secret' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(true) + }) + + it('rejects unknown privacy-protocol value', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '3', + 'auth-protocol': 'MD5', + 'auth-passphrase': 'secret', + 'privacy-protocol': 'UNKNOWN', + 'privacy-passphrase': 'priv' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('privacy-protocol'))).toBe(true) + }) +}) + +describe('validateTrapdXml – privacy-protocol without auth-protocol', () => { + it('returns error when privacy-protocol set but auth-protocol absent', () => { + const user = buildUser({ + 'security-name': 'user1', + 'security-level': '3', + 'privacy-protocol': 'AES', + 'privacy-passphrase': 'priv', + 'auth-passphrase': 'secret' + }) + const result = validateTrapdXml(buildXml({ users: user })) + expect(result.errors.some((e) => e.field.includes('auth-protocol'))).toBe(true) + }) +}) + +describe('validateTrapdXml – multiple snmpv3-user elements', () => { + it('is valid with multiple well-formed users', () => { + const user1 = buildUser({ 'security-name': 'userA', 'security-level': '1' }) + const user2 = buildUser({ + 'security-name': 'userB', + 'security-level': '2', + 'auth-protocol': 'MD5', + 'auth-passphrase': 'pass' + }) + const result = validateTrapdXml(buildXml({ users: user1 + user2 })) + expect(result.valid).toBe(true) + }) + + it('accumulates errors across multiple invalid users', () => { + // user[1]: missing security-name; user[2]: invalid security-level + const user1 = buildUser({ 'security-level': '1' }) + const user2 = buildUser({ 'security-name': 'userB', 'security-level': '99' }) + const result = validateTrapdXml(buildXml({ users: user1 + user2 })) + expect(result.valid).toBe(false) + expect(result.errors.some((e) => e.field.includes('snmpv3-user[1]'))).toBe(true) + expect(result.errors.some((e) => e.field.includes('snmpv3-user[2]'))).toBe(true) + }) +}) From 6193e7a863d6a37011662798d92f94b41ccd8ffa Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Sun, 29 Mar 2026 19:39:46 +0500 Subject: [PATCH 25/41] user updates changes --- .../netmgt/dao/api/TrapdConfigDao.java | 2 - .../dao/jaxb/DefaultTrapdConfigDao.java | 108 --- .../opennms/web/rest/v2/TrapdRestService.java | 133 +--- .../opennms/web/rest/v2/api/TrapdRestApi.java | 49 -- .../web/rest/v2/TrapdRestServiceIT.java | 691 +----------------- 5 files changed, 26 insertions(+), 957 deletions(-) diff --git a/opennms-dao-api/src/main/java/org/opennms/netmgt/dao/api/TrapdConfigDao.java b/opennms-dao-api/src/main/java/org/opennms/netmgt/dao/api/TrapdConfigDao.java index dcd1fcd65aeb..44bd5d4c1d8a 100644 --- a/opennms-dao-api/src/main/java/org/opennms/netmgt/dao/api/TrapdConfigDao.java +++ b/opennms-dao-api/src/main/java/org/opennms/netmgt/dao/api/TrapdConfigDao.java @@ -25,7 +25,5 @@ public interface TrapdConfigDao { TrapdConfiguration getConfig(); - TrapdConfiguration getMaskedConfig(); void updateConfig(TrapdConfiguration config); - void updateConfigWithoutUsers(TrapdConfiguration config); } diff --git a/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java b/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java index fb692ebf6ff8..d254f05af6fb 100644 --- a/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java +++ b/opennms-dao/src/main/java/org/opennms/netmgt/dao/jaxb/DefaultTrapdConfigDao.java @@ -56,11 +56,6 @@ public TrapdConfiguration getConfig() { return this.getConfig(this.getDefaultConfigId()); } - @Override - public TrapdConfiguration getMaskedConfig() { - return this.maskPassphrases(this.getConfig(this.getDefaultConfigId())); - } - @Override public void updateConfig(final TrapdConfiguration config) { this.updateConfig(this.getDefaultConfigId(), ConfigConvertUtil.objectToJson(config), true); @@ -75,107 +70,4 @@ public Consumer getUpdateCallback(){ public Consumer getValidationCallback() { return super.getValidationCallback(); } - - @Override - public void updateConfigWithoutUsers(TrapdConfiguration config) { - this.updateConfig(mergeTrapdConfiguration(config)); - } - - /** - * Merges the given payload into the current TrapdConfiguration. - * Only non-null fields in the payload will overwrite the current config. - * If payload is null, returns the current config as-is. - * - * @param payload the TrapdConfiguration with new values - * @return the merged TrapdConfiguration - */ - private TrapdConfiguration mergeTrapdConfiguration(final TrapdConfiguration payload) { - TrapdConfiguration config = this.getConfig(); - if (config == null) { - config = new TrapdConfiguration(); - } - if (payload == null) { - return config; - } - - // Merge String fields (null-safe, value-safe) - if (payload.getSnmpTrapAddress() != null && !java.util.Objects.equals(payload.getSnmpTrapAddress(), config.getSnmpTrapAddress())) { - config.setSnmpTrapAddress(payload.getSnmpTrapAddress()); - } - - // Merge int fields (always set, unless you want to skip 0) - if (payload.getSnmpTrapPort() != config.getSnmpTrapPort()) { - config.setSnmpTrapPort(payload.getSnmpTrapPort()); - } - if (payload.getThreads() != config.getThreads()) { - config.setThreads(payload.getThreads()); - } - if (payload.getQueueSize() != config.getQueueSize()) { - config.setQueueSize(payload.getQueueSize()); - } - if (payload.getBatchSize() != config.getBatchSize()) { - config.setBatchSize(payload.getBatchSize()); - } - if (payload.getBatchInterval() != config.getBatchInterval()) { - config.setBatchInterval(payload.getBatchInterval()); - } - - // Merge boolean fields (always set) - config.setNewSuspectOnTrap(payload.getNewSuspectOnTrap()); - config.setIncludeRawMessage(payload.isIncludeRawMessage()); - - // Merge useAddressFromVarbind (nullable Boolean) - try { - java.lang.reflect.Field field = payload.getClass().getDeclaredField("useAddressFromVarbind"); - field.setAccessible(true); - Boolean useAddressFromVarbind = (Boolean) field.get(payload); - if (useAddressFromVarbind != null) { - config.setUseAddressFromVarbind(useAddressFromVarbind); - } - } catch (Exception e) { - // ignore, do not set if not accessible - } - - // Merge snmpv3User list if present and not empty - java.util.List snmpv3UserList = payload.getSnmpv3UserCollection(); - if (snmpv3UserList != null && !snmpv3UserList.isEmpty()) { - config.setSnmpv3User(snmpv3UserList); - } - - return config; - } - - /** - * Returns a deep copy of the given {@link TrapdConfiguration} with - * {@code authPassphrase} and {@code privacyPassphrase} replaced by - * {@link #PASSPHRASE_PLACEHOLDER} so that real credentials are never - * exposed over the REST API. - * - * @param config the TrapdConfiguration to sanitize - * @return a sanitized deep copy with passphrases masked, or null if input is null - */ - private TrapdConfiguration maskPassphrases(final TrapdConfiguration config) { - if (config == null) { - return null; - } - TrapdConfiguration sanitized; - try { - sanitized = JaxbUtils.unmarshal( - TrapdConfiguration.class, JaxbUtils.marshal(config)); - } catch (Exception e) { - // Optionally log the error - throw new RuntimeException("Failed to deep copy TrapdConfiguration for masking", e); - } - if (sanitized.getSnmpv3UserCollection() != null) { - for (final Snmpv3User user : sanitized.getSnmpv3UserCollection()) { - if (!StringUtils.isBlank(user.getAuthPassphrase())) { - user.setAuthPassphrase(PASSPHRASE_PLACEHOLDER); - } - if (!StringUtils.isBlank(user.getPrivacyPassphrase())) { - user.setPrivacyPassphrase(PASSPHRASE_PLACEHOLDER); - } - } - } - return sanitized; - } } diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java index d5449a132814..1e2dca4c3958 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/TrapdRestService.java @@ -84,7 +84,7 @@ public Response uploadTrapdConfiguration(final Attachment attachment, final Secu @Override public Response getTrapdConfiguration(final SecurityContext securityContext) { try { - TrapdConfiguration config = trapdConfigDao.getMaskedConfig(); + TrapdConfiguration config = trapdConfigDao.getConfig(); if (config == null) { return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); } @@ -113,7 +113,7 @@ public Response updateTrapdConfiguration(TrapdConfigDto configDto, SecurityConte } try { - trapdConfigDao.updateConfigWithoutUsers(payload); + trapdConfigDao.updateConfig(payload); return Response.ok().build(); } catch (ValidationException e) { LOG.warn("Provided trapd configuration failed schema validation.", e); @@ -124,120 +124,6 @@ public Response updateTrapdConfiguration(TrapdConfigDto configDto, SecurityConte } } - @Override - public Response saveTrapdUser(Snmpv3UserDto user, SecurityContext securityContext) { - if (user == null) { - return Response.status(Status.BAD_REQUEST).entity("Missing SNMPv3 user in request body.").build(); - } - - try { - TrapdConfiguration config = trapdConfigDao.getMaskedConfig(); - if (config == null) { - return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); - } - - if (findUserIndexBySecurityName(config, user.getSecurityName()) >= 0) { - return Response.status(Status.CONFLICT) - .entity("SNMPv3 user with securityName '" + user.getSecurityName() + "' already exists.") - .build(); - } - - final String validationMessage = validateSnmpv3UserPayload(user); - if (validationMessage != null) { - return Response.status(Status.BAD_REQUEST).entity(validationMessage).build(); - } - - // Ensure required attributes are present when persisting a newly-created config. - if (!config.hasSnmpTrapPort()) { - config.setSnmpTrapPort(162); - } - if (!config.hasNewSuspectOnTrap()) { - config.setNewSuspectOnTrap(false); - } - - config.addSnmpv3User(user.toEntity()); - trapdConfigDao.updateConfig(config); - return Response.ok().build(); - } catch (ValidationException e) { - LOG.warn("Provided SNMPv3 user failed schema validation.", e); - return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); - } catch (Exception e) { - LOG.error("Failed to persist provided SNMPv3 user.", e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to persist trapd user.").build(); - } - } - - @Override - public Response updateTrapdUser(String securityName, Snmpv3UserDto user, SecurityContext securityContext) { - if (StringUtils.isBlank(securityName)) { - return Response.status(Status.BAD_REQUEST).entity("Valid security name is required.").build(); - } - - if (user == null) { - return Response.status(Status.BAD_REQUEST).entity("Missing SNMPv3 user in request body.").build(); - } - - try { - final TrapdConfiguration config = trapdConfigDao.getMaskedConfig(); - if (config == null) { - return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); - } - - final int index = findUserIndexBySecurityName(config, securityName); - if (index < 0) { - return Response.status(Status.NOT_FOUND) - .entity("SNMPv3 user with securityName '" + securityName + "' was not found.") - .build(); - } - - final String validationMessage = validateSnmpv3UserPayload(user); - if (validationMessage != null) { - return Response.status(Status.BAD_REQUEST).entity(validationMessage).build(); - } - - config.setSnmpv3User(index, user.toEntity()); - trapdConfigDao.updateConfig(config); - return Response.ok().build(); - } catch (ValidationException e) { - LOG.warn("Updating SNMPv3 user failed schema validation.", e); - return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); - } catch (Exception e) { - LOG.error("Failed to update SNMPv3 user for securityName {}.", securityName, e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to update trapd user.").build(); - } - } - - @Override - public Response deleteTrapdUser(String securityName, SecurityContext securityContext) { - if (StringUtils.isBlank(securityName)) { - return Response.status(Status.BAD_REQUEST).entity("Valid security name is required.").build(); - } - - try { - final TrapdConfiguration config = trapdConfigDao.getMaskedConfig(); - if (config == null) { - return Response.status(Status.NOT_FOUND).entity("Trapd configuration not found.").build(); - } - - final int index = findUserIndexBySecurityName(config, securityName); - if (index < 0) { - return Response.status(Status.NOT_FOUND) - .entity("SNMPv3 user with securityName '" + securityName + "' was not found.") - .build(); - } - - config.removeSnmpv3UserAt(index); - trapdConfigDao.updateConfig(config); - return Response.noContent().build(); - } catch (ValidationException e) { - LOG.warn("Removing SNMPv3 user failed schema validation.", e); - return Response.status(Status.BAD_REQUEST).entity(e.getMessage()).build(); - } catch (Exception e) { - LOG.error("Failed to delete SNMPv3 user for securityName {}.", securityName, e); - return Response.status(Status.INTERNAL_SERVER_ERROR).entity("Failed to delete trapd user.").build(); - } - } - private String validateTrapdConfigDtoFields(TrapdConfigDto configDto) { if (StringUtils.isBlank(configDto.getSnmpTrapAddress())) { return "snmpTrapAddress is required."; @@ -257,17 +143,16 @@ private String validateTrapdConfigDtoFields(TrapdConfigDto configDto) { if (configDto.getBatchInterval() != null && configDto.getBatchInterval() < 0) { return "batchInterval must be non-negative."; } - return null; - } - private int findUserIndexBySecurityName(final TrapdConfiguration config, final String securityName) { - for (int i = 0; i < config.getSnmpv3UserCount(); i++) { - final Snmpv3User existing = config.getSnmpv3User(i); - if (StringUtils.equals(existing.getSecurityName(), securityName)) { - return i; + if (configDto.getSnmpv3User() != null) { + for (Snmpv3UserDto user : configDto.getSnmpv3User()) { + String userValidation = validateSnmpv3UserPayload(user); + if (userValidation != null) { + return "Invalid SNMPv3 user: " + user.getSecurityName() + ". " + userValidation; + } } } - return -1; + return null; } private String validateTrapdConfigurationPayload(final TrapdConfiguration config) { diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java index f18ca74b84d9..6c40d277b571 100644 --- a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/TrapdRestApi.java @@ -94,54 +94,5 @@ public interface TrapdRestApi { @ApiResponse(responseCode = "500", description = "Failed to update trapd configuration") }) Response updateTrapdConfiguration(TrapdConfigDto payload, @Context SecurityContext securityContext); - - @POST - @Path("user") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Save SNMPv3 user", - description = "Save SNMPv3 user configuration with provided JSON payload.", - operationId = "saveTrapdUser" - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User saved successfully"), - @ApiResponse(responseCode = "400", description = "Invalid user payload"), - @ApiResponse(responseCode = "409", description = "SNMPv3 user with provided securityName already exists"), - @ApiResponse(responseCode = "500", description = "Failed to save user") - }) - Response saveTrapdUser(Snmpv3UserDto user, @Context SecurityContext securityContext); - - @PUT - @Path("user/{securityName}") - @Consumes(MediaType.APPLICATION_JSON) - @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Update SNMPv3 user", - description = "Update SNMPv3 user configuration for the provided securityName.", - operationId = "updateTrapdUser" - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "User updated successfully"), - @ApiResponse(responseCode = "400", description = "Invalid user payload"), - @ApiResponse(responseCode = "404", description = "SNMPv3 user with provided securityName was not found"), - @ApiResponse(responseCode = "500", description = "Failed to update user") - }) - Response updateTrapdUser(@PathParam("securityName") String securityName, Snmpv3UserDto user, @Context SecurityContext securityContext); - - @DELETE - @Path("user/{securityName}") - @Operation( - summary = "Delete SNMPv3 user", - description = "Delete SNMPv3 user configuration with provided securityName.", - operationId = "deleteTrapdUser" - ) - @ApiResponses(value = { - @ApiResponse(responseCode = "204", description = "User deleted successfully"), - @ApiResponse(responseCode = "400", description = "Invalid securityName"), - @ApiResponse(responseCode = "404", description = "SNMPv3 user with provided securityName was not found"), - @ApiResponse(responseCode = "500", description = "Failed to delete user") - }) - Response deleteTrapdUser(@PathParam("securityName") String securityName, @Context SecurityContext securityContext); } diff --git a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java index 389edc71b86d..70f0068d3b6d 100644 --- a/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java +++ b/opennms-webapp-rest/src/test/java/org/opennms/web/rest/v2/TrapdRestServiceIT.java @@ -172,7 +172,7 @@ public void getShouldReturnOkWithConfigWhenExists() { config.setSnmpTrapPort(162); config.setSnmpTrapAddress("127.0.0.1"); config.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -185,7 +185,7 @@ public void getShouldReturnOkWithConfigWhenExists() { @Test public void getShouldReturnNotFoundWhenNoConfigurationExists() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(null); + when(trapdConfigDao.getConfig()).thenReturn(null); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); @@ -195,7 +195,7 @@ public void getShouldReturnNotFoundWhenNoConfigurationExists() { @Test public void getShouldReturnServerErrorWhenExceptionThrown() { - org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(trapdConfigDao).getMaskedConfig(); + org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(trapdConfigDao).getConfig(); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); @@ -216,7 +216,7 @@ public void getShouldMaskBothPassphrasesWithPlaceholder() { user.setPrivacyProtocol("AES"); user.setPrivacyPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -236,7 +236,7 @@ public void getShouldMaskAuthPassphraseWhenPrivacyPassphraseIsAbsent() { user.setAuthProtocol("MD5"); user.setAuthPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -255,7 +255,7 @@ public void getShouldNotSetPlaceholderWhenPassphrasesAreNull() { user.setSecurityLevel(1); // no auth or privacy passphrase set config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -286,7 +286,7 @@ public void getShouldMaskPassphrasesForEveryUser() { userB.setAuthPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(userB); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -310,13 +310,13 @@ public void getShouldNotMutateStoredConfigWhenMaskingPassphrases() { user.setPrivacyProtocol("AES"); user.setPrivacyPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } - // The config returned by getMaskedConfig should not have been mutated by the service + // The config returned by getConfig should not have been mutated by the service assertEquals(PASSPHRASE_PLACEHOLDER, config.getSnmpv3User(0).getAuthPassphrase()); assertEquals(PASSPHRASE_PLACEHOLDER, config.getSnmpv3User(0).getPrivacyPassphrase()); } @@ -333,7 +333,7 @@ public void getShouldReturnConfigWithOtherFieldsIntactAfterMasking() { user.setPrivacyProtocol("AES"); user.setPrivacyPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); @@ -359,663 +359,6 @@ private static TrapdConfiguration buildMinimalConfig() { return config; } - @Test - public void saveUserShouldReturnBadRequestWhenPayloadMissing() { - try (Response response = trapdRestService.saveTrapdUser(null, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Missing SNMPv3 user in request body.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldPersistUserAndReturnOk() { - TrapdConfiguration existing = new TrapdConfiguration(); - existing.setSnmpTrapPort(1162); - existing.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(existing); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setEngineId("8000000001020304"); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(3); - userDto.setAuthProtocol("SHA"); - userDto.setAuthPassphrase("auth-pass"); - userDto.setPrivacyProtocol("AES"); - userDto.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - assertNull(response.getEntity()); - } - - ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfig(captor.capture()); - TrapdConfiguration persisted = captor.getValue(); - assertEquals(1, persisted.getSnmpv3UserCount()); - assertEquals("opennms-user", persisted.getSnmpv3User(0).getSecurityName()); - } - - @Test - public void saveUserShouldRejectWhenSecurityNameMissing() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityName is required.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenSecurityLevelMissing() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel is required.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenSecurityLevelThreeMissingPrivacy() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(3); - userDto.setAuthProtocol("SHA"); - userDto.setAuthPassphrase("auth-pass"); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel 3 requires both auth and privacy credentials.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldReturnBadRequestWhenValidationFails() { - TrapdConfiguration existing = new TrapdConfiguration(); - existing.setSnmpTrapPort(1162); - existing.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(existing); - whenValidationFailsOnUpdate("user validation failed"); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("user validation failed", response.getEntity()); - } - } - - @Test - public void saveUserShouldReturnServerErrorWhenPersistenceThrows() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); - assertEquals("Failed to persist trapd user.", response.getEntity()); - } - } - - @Test - public void saveUserShouldReturnNotFoundWhenNoConfigExists() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(null); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("Trapd configuration not found.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldReturnConflictWhenSecurityNameAlreadyExists() { - TrapdConfiguration existing = new TrapdConfiguration(); - existing.setSnmpTrapPort(1162); - existing.setNewSuspectOnTrap(false); - Snmpv3User existingUser = new Snmpv3User(); - existingUser.setSecurityName("duplicate-user"); - existing.addSnmpv3User(existingUser); - when(trapdConfigDao.getMaskedConfig()).thenReturn(existing); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("duplicate-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); - assertEquals("SNMPv3 user with securityName 'duplicate-user' already exists.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - // --- validateSnmpv3UserPayload rule tests --- - - @Test - public void saveUserShouldRejectWhenSecurityLevelOutOfRange() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("opennms-user"); - userDto.setSecurityLevel(5); - - try (Response response = trapdRestService.saveTrapdUser(userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel must be between 1 and 3.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenAuthProtocolIsInvalid() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(2); - user.setAuthProtocol("MD2"); - user.setAuthPassphrase("auth-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Unsupported authProtocol.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenPrivacyProtocolIsInvalid() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(3); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - user.setPrivacyProtocol("3DES"); - user.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Unsupported privacyProtocol.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenAuthProtocolProvidedWithoutPassphrase() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(2); - user.setAuthProtocol("SHA"); - // no authPassphrase - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("authProtocol and authPassphrase must be provided together.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenPrivacyProtocolProvidedWithoutPassphrase() { - // Provide a valid TrapdConfiguration with required fields - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(3); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - // no privacyProtocol - user.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("privacyProtocol and privacyPassphrase must be provided together.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenAuthPassphraseProvidedWithoutProtocol() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(2); - // no authProtocol - user.setAuthPassphrase("auth-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("authProtocol and authPassphrase must be provided together.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenPrivacyPassphraseProvidedWithoutProtocol() { - // Provide a valid TrapdConfiguration with required fields - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(3); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - // no privacyProtocol - user.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("privacyProtocol and privacyPassphrase must be provided together.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenSecurityLevelOneHasAuthCredentials() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(1); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel 1 does not allow auth or privacy credentials.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenSecurityLevelTwoMissingAuthCredentials() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(2); - // no auth protocol/passphrase - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel 2 requires auth credentials and does not allow privacy credentials.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void saveUserShouldRejectWhenSecurityLevelTwoHasPrivacyCredentials() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(new TrapdConfiguration()); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - user.setSecurityLevel(2); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - user.setPrivacyProtocol("AES"); - user.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.saveTrapdUser(user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel 2 requires auth credentials and does not allow privacy credentials.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void deleteUserShouldReturnBadRequestWhenSecurityNameNull() { - try (Response response = trapdRestService.deleteTrapdUser(null, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Valid security name is required.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void deleteUserShouldReturnBadRequestWhenSecurityNameBlank() { - try (Response response = trapdRestService.deleteTrapdUser(" ", null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Valid security name is required.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void deleteUserShouldReturnNotFoundWhenNoConfig() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(null); - - try (Response response = trapdRestService.deleteTrapdUser("missing-user", null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("Trapd configuration not found.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void deleteUserShouldReturnNotFoundWhenSecurityNameMissing() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - try (Response response = trapdRestService.deleteTrapdUser("missing-user", null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("SNMPv3 user with securityName 'missing-user' was not found.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void deleteUserShouldRemoveUserAndReturnNoContent() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User user = new Snmpv3User(); - user.setSecurityName("user-to-delete"); - user.setSecurityLevel(1); - config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - try (Response response = trapdRestService.deleteTrapdUser("user-to-delete", null)) { - assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); - assertNull(response.getEntity()); - } - - ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfig(captor.capture()); - assertEquals(0, captor.getValue().getSnmpv3UserCount()); - } - - @Test - public void deleteUserShouldRemoveOnlyMatchingSecurityName() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User keep = new Snmpv3User(); - keep.setSecurityName("keep-user"); - Snmpv3User remove = new Snmpv3User(); - remove.setSecurityName("remove-user"); - config.addSnmpv3User(keep); - config.addSnmpv3User(remove); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - try (Response response = trapdRestService.deleteTrapdUser("remove-user", null)) { - assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); - } - - ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfig(captor.capture()); - assertEquals(1, captor.getValue().getSnmpv3UserCount()); - assertEquals("keep-user", captor.getValue().getSnmpv3User(0).getSecurityName()); - } - - @Test - public void deleteUserShouldReturnBadRequestWhenValidationFails() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User user = new Snmpv3User(); - user.setSecurityName("test-user"); - config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - whenValidationFailsOnUpdate("delete validation error"); - - try (Response response = trapdRestService.deleteTrapdUser("test-user", null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("delete validation error", response.getEntity()); - } - } - - @Test - public void deleteUserShouldReturnServerErrorWhenPersistenceThrows() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User user = new Snmpv3User(); - user.setSecurityName("test-user"); - config.addSnmpv3User(user); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - - try (Response response = trapdRestService.deleteTrapdUser("test-user", null)) { - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); - assertEquals("Failed to delete trapd user.", response.getEntity()); - } - } - - @Test - public void updateUserShouldReturnBadRequestWhenSecurityNameNull() { - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - - try (Response response = trapdRestService.updateTrapdUser(null, user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Valid security name is required.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnBadRequestWhenSecurityNameBlank() { - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - - try (Response response = trapdRestService.updateTrapdUser("", user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Valid security name is required.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnBadRequestWhenPathAndPayloadSecurityNameMismatch() { - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("payload-user"); - user.setSecurityLevel(1); - - // The service checks for Trapd configuration first, so if not found, it returns 404 - try (Response response = trapdRestService.updateTrapdUser("path-user", user, null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("Trapd configuration not found.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnBadRequestWhenPayloadNull() { - try (Response response = trapdRestService.updateTrapdUser("existing-user", null, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("Missing SNMPv3 user in request body.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnNotFoundWhenNoConfig() { - when(trapdConfigDao.getMaskedConfig()).thenReturn(null); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("opennms-user"); - - try (Response response = trapdRestService.updateTrapdUser("opennms-user", user, null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("Trapd configuration not found.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnNotFoundWhenSecurityNameMissing() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("missing-user"); - - try (Response response = trapdRestService.updateTrapdUser("missing-user", user, null)) { - assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); - assertEquals("SNMPv3 user with securityName 'missing-user' was not found.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnBadRequestWhenSecurityLevelMissing() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User existing = new Snmpv3User(); - existing.setSecurityName("existing-user"); - config.addSnmpv3User(existing); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("existing-user"); - - try (Response response = trapdRestService.updateTrapdUser("existing-user", user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel is required.", response.getEntity()); - } - - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReturnBadRequestWhenPayloadValidationFails() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User existing = new Snmpv3User(); - existing.setSecurityName("existing-user"); - config.addSnmpv3User(existing); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - // securityLevel 3 requires privacy — missing privacy intentionally - Snmpv3UserDto user = new Snmpv3UserDto(); - user.setSecurityName("existing-user"); - user.setSecurityLevel(3); - user.setAuthProtocol("SHA"); - user.setAuthPassphrase("auth-pass"); - - try (Response response = trapdRestService.updateTrapdUser("existing-user", user, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("securityLevel 3 requires both auth and privacy credentials.", response.getEntity()); - } - verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - } - - @Test - public void updateUserShouldReplaceUserBySecurityNameAndReturnOk() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User existing = new Snmpv3User(); - existing.setSecurityName("old-user"); - existing.setSecurityLevel(1); - config.addSnmpv3User(existing); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - - Snmpv3UserDto updatedDto = new Snmpv3UserDto(); - updatedDto.setSecurityName("old-user"); - updatedDto.setSecurityLevel(3); - updatedDto.setAuthProtocol("SHA"); - updatedDto.setAuthPassphrase("auth-pass"); - updatedDto.setPrivacyProtocol("AES"); - updatedDto.setPrivacyPassphrase("priv-pass"); - - try (Response response = trapdRestService.updateTrapdUser("old-user", updatedDto, null)) { - assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); - assertNull(response.getEntity()); - } - - ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfig(captor.capture()); - assertEquals(1, captor.getValue().getSnmpv3UserCount()); - assertEquals("old-user", captor.getValue().getSnmpv3User(0).getSecurityName()); - } - - @Test - public void updateUserShouldReturnBadRequestWhenSchemaValidationFails() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User existing = new Snmpv3User(); - existing.setSecurityName("existing-user"); - config.addSnmpv3User(existing); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - whenValidationFailsOnUpdate("schema update error"); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("existing-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.updateTrapdUser("existing-user", userDto, null)) { - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertEquals("schema update error", response.getEntity()); - } - } - - @Test - public void updateUserShouldReturnServerErrorWhenPersistenceThrows() { - TrapdConfiguration config = new TrapdConfiguration(); - config.setSnmpTrapPort(162); - config.setNewSuspectOnTrap(false); - Snmpv3User existing = new Snmpv3User(); - existing.setSecurityName("existing-user"); - config.addSnmpv3User(existing); - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); - org.mockito.Mockito.doThrow(new RuntimeException("db down")).when(trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); - - Snmpv3UserDto userDto = new Snmpv3UserDto(); - userDto.setSecurityName("existing-user"); - userDto.setSecurityLevel(1); - - try (Response response = trapdRestService.updateTrapdUser("existing-user", userDto, null)) { - assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); - assertEquals("Failed to update trapd user.", response.getEntity()); - } - } - @Test public void updateShouldReturnBadRequestWhenPayloadMissing() { try (Response response = trapdRestService.updateTrapdConfiguration(null, null)) { @@ -1035,7 +378,7 @@ public void updateShouldReturnBadRequestWhenSnmpTrapPortMissing() { assertEquals("snmpTrapPort is required and must be between 1 and 65535.", response.getEntity()); } - verify(trapdConfigDao, never()).updateConfigWithoutUsers(org.mockito.Mockito.any(TrapdConfiguration.class)); + verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); } @Test @@ -1049,7 +392,7 @@ public void updateShouldReturnBadRequestWhenNewSuspectOnTrapMissing() { assertEquals("newSuspectOnTrap is required.", response.getEntity()); } - verify(trapdConfigDao, never()).updateConfigWithoutUsers(org.mockito.Mockito.any(TrapdConfiguration.class)); + verify(trapdConfigDao, never()).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); } @Test @@ -1068,7 +411,7 @@ public void updateShouldMergePayloadAndPersist() { } ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfigWithoutUsers(captor.capture()); + verify(trapdConfigDao).updateConfig(captor.capture()); TrapdConfiguration persisted = captor.getValue(); assertEquals(10164, persisted.getSnmpTrapPort()); assertEquals(4, persisted.getThreads()); @@ -1089,14 +432,14 @@ public void updateShouldPersistUseAddressFromVarbindWhenProvided() { } ArgumentCaptor captor = ArgumentCaptor.forClass(TrapdConfiguration.class); - verify(trapdConfigDao).updateConfigWithoutUsers(captor.capture()); + verify(trapdConfigDao).updateConfig(captor.capture()); assertTrue(captor.getValue().shouldUseAddressFromVarbind()); } @Test public void updateShouldReturnBadRequestWhenValidationFails() { org.mockito.Mockito.doThrow(new ValidationException("validation failed")) - .when(trapdConfigDao).updateConfigWithoutUsers(org.mockito.Mockito.any(TrapdConfiguration.class)); + .when(trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); TrapdConfigDto payload = new TrapdConfigDto(); payload.setSnmpTrapAddress("127.0.0.1"); @@ -1112,7 +455,7 @@ public void updateShouldReturnBadRequestWhenValidationFails() { @Test public void updateShouldReturnServerErrorWhenPersistenceThrows() { org.mockito.Mockito.doThrow(new RuntimeException("db down")) - .when(trapdConfigDao).updateConfigWithoutUsers(org.mockito.Mockito.any(TrapdConfiguration.class)); + .when(trapdConfigDao).updateConfig(org.mockito.Mockito.any(TrapdConfiguration.class)); TrapdConfigDto payload = new TrapdConfigDto(); payload.setSnmpTrapAddress("127.0.0.1"); @@ -1256,7 +599,7 @@ public void getShouldHandleLargeNumberOfSnmpv3Users() { user.setPrivacyPassphrase(PASSPHRASE_PLACEHOLDER); config.addSnmpv3User(user); } - when(trapdConfigDao.getMaskedConfig()).thenReturn(config); + when(trapdConfigDao.getConfig()).thenReturn(config); try (Response response = trapdRestService.getTrapdConfiguration(null)) { assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); TrapdConfigDto returned = (TrapdConfigDto) response.getEntity(); From e583a11363ef6eddb117d7529bf75174aff22170 Mon Sep 17 00:00:00 2001 From: Shahbaz Date: Sun, 29 Mar 2026 21:29:37 +0500 Subject: [PATCH 26/41] scv changes and test coverage --- ui/src/components/SCV/ScvSearchDrawer.vue | 169 ++++++++++++++++++ .../TrapConfiguration/CreateSnmpV3User.vue | 100 +++++++---- .../Dialog/DeleteUserConfirmationDialog.vue | 9 +- .../GeneralConfiguration.vue | 9 +- .../SnmpV3UserManagement.vue | 32 ++-- ui/src/services/trapdConfigurationService.ts | 49 +---- ui/src/stores/trapConfigStore.ts | 11 +- ui/src/types/trapConfig.d.ts | 17 +- .../CreateSnmpV3User.test.ts | 81 ++++++--- .../GeneralConfiguration.test.ts | 28 ++- .../SnmpV3UserManagement.test.ts | 89 ++++++--- ui/tests/stores/trapConfigStore.test.ts | 126 +++++++++++++ 12 files changed, 557 insertions(+), 163 deletions(-) create mode 100644 ui/src/components/SCV/ScvSearchDrawer.vue create mode 100644 ui/tests/stores/trapConfigStore.test.ts diff --git a/ui/src/components/SCV/ScvSearchDrawer.vue b/ui/src/components/SCV/ScvSearchDrawer.vue new file mode 100644 index 000000000000..66638dde7c53 --- /dev/null +++ b/ui/src/components/SCV/ScvSearchDrawer.vue @@ -0,0 +1,169 @@ + + + + + + diff --git a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue index d60d6e4ed329..34f56ec54d86 100644 --- a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue +++ b/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue @@ -75,7 +75,7 @@ /> @@ -102,7 +102,7 @@ /> @@ -124,7 +124,12 @@ {{ store.createUserDrawerState.mode === CreateEditMode.Create ? 'Create User' : 'Update User' }} - + + @@ -132,7 +137,8 @@ import useSnackbar from '@/composables/useSnackbar' import { AUTH_PROTOCOL_OPTIONS, PRIVACY_PROTOCOL_OPTIONS, SECURITY_LEVEL_OPTIONS, SecurityLevel } from '@/lib/trapdValidator' import { mapUserToServer } from '@/mappers/trapdConfig.mapper' -import { saveTrapdUser, updateTrapdUser } from '@/services/trapdConfigurationService' +import { updateTrapdConfiguration } from '@/services/trapdConfigurationService' +import { useScvStore } from '@/stores/scvStore' import { useTrapConfigStore } from '@/stores/trapConfigStore' import { CreateEditMode } from '@/types' import type { SnmpV3UserError } from '@/types/trapConfig' @@ -143,7 +149,7 @@ import { FeatherInput } from '@featherds/input' import { FeatherSelect, ISelectItemType } from '@featherds/select' import TableCard from '../Common/TableCard.vue' import ScvInputIcon from '../SCV/ScvInputIcon.vue' -import SearchExistingCredential from './Drawer/SearchExistingCredential.vue' +import ScvSearchDrawer from '../SCV/ScvSearchDrawer.vue' const store = useTrapConfigStore() const { showSnackBar } = useSnackbar() @@ -158,6 +164,7 @@ const privacyPassphrase = ref('') const isSaveDisabled = ref(true) const isSaving = ref(false) const error = ref({}) +const scvStore = useScvStore() const authProtocolVisible = computed(() => { const selectedSecurityLevel = Number(securityLevel.value?._value) @@ -169,25 +176,6 @@ const privacyProtocolVisible = computed(() => { return selectedSecurityLevel === SecurityLevel.AuthPriv }) -watch(securityLevel, (selectedSecurityLevel) => { - const levelValue = Number(selectedSecurityLevel?._value) - - if (levelValue !== SecurityLevel.AuthNoPriv && levelValue !== SecurityLevel.AuthPriv) { - authProtocol.value = createEmptySelectItem() - authPassphrase.value = '' - } - - if (levelValue !== SecurityLevel.AuthPriv) { - authProtocol.value = createEmptySelectItem() - privacyProtocol.value = createEmptySelectItem() - authPassphrase.value = '' - privacyPassphrase.value = '' - } - - error.value = validateInputs() - isSaveDisabled.value = Object.keys(error.value).length > 0 -}) - const saveUser = async () => { const validationError = validateInputs() if (Object.keys(validationError).length > 0) { @@ -213,14 +201,25 @@ const saveUser = async () => { isSaving.value = true if (store.createUserDrawerState.mode === CreateEditMode.Create) { - await saveTrapdUser(payload) - } else if (store.createUserDrawerState.mode === CreateEditMode.Edit) { - const selectedUser = store.SnmpV3Users?.[store.createUserDrawerState.selectedUserIndex] - if (!selectedUser?.securityName) { - throw new Error('Unable to determine the selected SNMPv3 user to update.') + const updatedConfig = { + ...store.trapdConfig, + snmpv3User: [...(store.trapdConfig.snmpv3User || []), payload] } - - await updateTrapdUser(selectedUser.securityName, payload) + await updateTrapdConfiguration(updatedConfig) + } + if (store.createUserDrawerState.mode === CreateEditMode.Edit) { + const selectedUser = store.snmpV3Users?.[store.createUserDrawerState.selectedUserIndex] + if (!selectedUser) { + showSnackBar({ msg: 'Unable to determine the selected SNMPv3 user to update.', error: true }) + return + } + const updatedUsers = [...(store.trapdConfig.snmpv3User || [])] + updatedUsers[store.createUserDrawerState.selectedUserIndex] = payload + const updatedConfig = { + ...store.trapdConfig, + snmpv3User: updatedUsers + } + await updateTrapdConfiguration(updatedConfig) } await store.fetchTrapConfig() @@ -236,6 +235,16 @@ const saveUser = async () => { isSaving.value = false } } +const scvItemSelected = (item: any) => { + const scvValue = '${scv:' + item.alias + ':' + item.key + '}' + + if (store.credentialDrawerState.key === 'auth') { + authPassphrase.value = scvValue + } else if (store.credentialDrawerState.key === 'privacy') { + privacyPassphrase.value = scvValue + } + store.closeCredentialDrawer() +} const validateInputs = () => { const newError: SnmpV3UserError = {} @@ -266,13 +275,14 @@ const validateInputs = () => { return newError } -const loadUserData = (drawerState: typeof store.createUserDrawerState) => { +const loadUserData = async (drawerState: typeof store.createUserDrawerState) => { if (drawerState.mode === CreateEditMode.Edit && drawerState.selectedUserIndex > -1) { - const selectedUser = store.SnmpV3Users ? store.SnmpV3Users[drawerState.selectedUserIndex] : null + const selectedUser = store.snmpV3Users ? store.snmpV3Users[drawerState.selectedUserIndex] : null if (selectedUser) { const selectedSecurityLevel = Number(selectedUser.securityLevel) securityLevel.value = SECURITY_LEVEL_OPTIONS.find(option => option._value === String(selectedSecurityLevel)) ?? createEmptySelectItem() + await nextTick() authProtocol.value = (selectedSecurityLevel === SecurityLevel.AuthNoPriv || selectedSecurityLevel === SecurityLevel.AuthPriv) ? AUTH_PROTOCOL_OPTIONS.find(option => option._value === selectedUser.authProtocol) ?? createEmptySelectItem() : createEmptySelectItem() @@ -294,6 +304,26 @@ const loadUserData = (drawerState: typeof store.createUserDrawerState) => { privacyPassphrase.value = '' } } + +watch(securityLevel, (selectedSecurityLevel) => { + const levelValue = Number(selectedSecurityLevel?._value) + + if (levelValue !== SecurityLevel.AuthNoPriv && levelValue !== SecurityLevel.AuthPriv) { + authProtocol.value = createEmptySelectItem() + authPassphrase.value = '' + } + + if (levelValue !== SecurityLevel.AuthPriv) { + authProtocol.value = createEmptySelectItem() + privacyProtocol.value = createEmptySelectItem() + authPassphrase.value = '' + privacyPassphrase.value = '' + } + + error.value = validateInputs() + isSaveDisabled.value = Object.keys(error.value).length > 0 +}) + watchEffect(() => { error.value = validateInputs() isSaveDisabled.value = Object.keys(error.value).length > 0 @@ -304,6 +334,10 @@ watch( loadUserData(store.createUserDrawerState) }, { deep: true, immediate: true } ) + +onMounted(() => { + scvStore.populate() +}) - diff --git a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue b/ui/src/components/TrapdConfiguration/CreateSnmpV3User.vue similarity index 99% rename from ui/src/components/TrapConfiguration/CreateSnmpV3User.vue rename to ui/src/components/TrapdConfiguration/CreateSnmpV3User.vue index 34f56ec54d86..fa4175c70c51 100644 --- a/ui/src/components/TrapConfiguration/CreateSnmpV3User.vue +++ b/ui/src/components/TrapdConfiguration/CreateSnmpV3User.vue @@ -139,7 +139,7 @@ import { AUTH_PROTOCOL_OPTIONS, PRIVACY_PROTOCOL_OPTIONS, SECURITY_LEVEL_OPTIONS import { mapUserToServer } from '@/mappers/trapdConfig.mapper' import { updateTrapdConfiguration } from '@/services/trapdConfigurationService' import { useScvStore } from '@/stores/scvStore' -import { useTrapConfigStore } from '@/stores/trapConfigStore' +import { useTrapdConfigStore } from '@/stores/trapdConfigStore' import { CreateEditMode } from '@/types' import type { SnmpV3UserError } from '@/types/trapConfig' import { FeatherButton } from '@featherds/button' @@ -151,7 +151,7 @@ import TableCard from '../Common/TableCard.vue' import ScvInputIcon from '../SCV/ScvInputIcon.vue' import ScvSearchDrawer from '../SCV/ScvSearchDrawer.vue' -const store = useTrapConfigStore() +const store = useTrapdConfigStore() const { showSnackBar } = useSnackbar() const createEmptySelectItem = (): ISelectItemType => (undefined as unknown as ISelectItemType) const securityName = ref('') diff --git a/ui/src/components/TrapConfiguration/Dialog/DeleteUserConfirmationDialog.vue b/ui/src/components/TrapdConfiguration/Dialog/DeleteUserConfirmationDialog.vue similarity index 92% rename from ui/src/components/TrapConfiguration/Dialog/DeleteUserConfirmationDialog.vue rename to ui/src/components/TrapdConfiguration/Dialog/DeleteUserConfirmationDialog.vue index 685cfa0e58ae..5fbd09d69543 100644 --- a/ui/src/components/TrapConfiguration/Dialog/DeleteUserConfirmationDialog.vue +++ b/ui/src/components/TrapdConfiguration/Dialog/DeleteUserConfirmationDialog.vue @@ -29,15 +29,15 @@