From 0558ef239b28cc67eb4114616f5c9551a692c405 Mon Sep 17 00:00:00 2001 From: Markus Scully Date: Thu, 27 Jul 2023 12:24:26 +0200 Subject: [PATCH] Add update policy command --- docs/_docs/hivemq/policies.md | 50 +++++++- .../hivemq/policies/UpdatePolicyCommand.java | 107 ++++++++++++++++ .../cli/hivemq/policies/UpdatePolicyTask.java | 65 ++++++++++ .../java/com/hivemq/cli/ioc/HiveMqModule.java | 3 + .../policies/UpdatePolicyCommandTest.java | 116 ++++++++++++++++++ .../hivemq/policies/UpdatePolicyTaskTest.java | 83 +++++++++++++ 6 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommand.java create mode 100644 src/main/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTask.java create mode 100644 src/test/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommandTest.java create mode 100644 src/test/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTaskTest.java diff --git a/docs/_docs/hivemq/policies.md b/docs/_docs/hivemq/policies.md index 54957dc5b..816242a57 100644 --- a/docs/_docs/hivemq/policies.md +++ b/docs/_docs/hivemq/policies.md @@ -22,6 +22,7 @@ mqtt hivemq policies |---------|-------------------------------------| | create | See [Create Policy](#create-policy) | | get | See [Get Policy](#get-policy) | +| update | See [Update Policy](#update-policy) | | list | See [List Policies](#list-policies) | | delete | See [Delete Policy](#delete-policy) | @@ -58,8 +59,8 @@ mqtt hivemq policies create --file my-policy.json ## Options -| Option | Long Version | Explanation | Required | -|--------|----------------|-----------------------------------------------------------------------------------------------------|-------------------------------------------------| +| Option | Long Version | Explanation | Required | +|--------|----------------|-----------------------------------------------------------------------------------------------------|:-----------------------------------------------:| | | `--definition` | The definition of the policy. This should be a JSON string containing a complete policy definition. | Either `--definition` or `--file`, but not both | | | `--file` | A path to a file containing the definition of the policy. | Either `--definition` or `--file`, but not both | @@ -144,6 +145,51 @@ $ mqtt hivemq policies get --id my-policy-id {% include options/help-options.md defaultHelp=true %} +*** + +# Update Policy + +*** + +Update an existing policy. +The policy definition may be provided either directly from the command line or from a file. +The provided policy ID argument must match the ID in the policy definition. + +``` +mqtt hivemq policies update +``` + +*** + +## Simple Example + +``` +mqtt hivemq policies update --id my-policy-id --file my-policy.json +``` + +*** + +## Options + +| Option | Long Version | Explanation | Required | +|--------|----------------|-----------------------------------------------------------------------------------------------------|:-----------------------------------------------:| +| `-i` | `--id` | The id of the policy to be updated. | X | +| | `--definition` | The definition of the policy. This should be a JSON string containing a complete policy definition. | Either `--definition` or `--file`, but not both | +| | `--file` | A path to a file containing the definition of the policy. | Either `--definition` or `--file`, but not both | + +### API Connection Options + +{% include options/api-connection-options.md %} + +### Logging Options + +{% include options/logging-options.md %} + +### Help Options + +{% include options/help-options.md defaultHelp=true %} + + *** # List Policies diff --git a/src/main/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommand.java b/src/main/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommand.java new file mode 100644 index 000000000..576e759e9 --- /dev/null +++ b/src/main/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommand.java @@ -0,0 +1,107 @@ +/* + * Copyright 2019-present HiveMQ and the HiveMQ Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.hivemq.cli.commands.hivemq.policies; + +import com.google.gson.Gson; +import com.hivemq.cli.MqttCLIMain; +import com.hivemq.cli.commands.hivemq.datagovernance.DataGovernanceOptions; +import com.hivemq.cli.commands.hivemq.datagovernance.OutputFormatter; +import com.hivemq.cli.commands.hivemq.datagovernance.PolicyDefinitionOptions; +import com.hivemq.cli.hivemq.policies.UpdatePolicyTask; +import com.hivemq.cli.openapi.hivemq.DataGovernanceHubPoliciesApi; +import com.hivemq.cli.rest.HiveMQRestService; +import org.jetbrains.annotations.NotNull; +import org.tinylog.Logger; +import picocli.CommandLine; + +import javax.inject.Inject; +import java.util.concurrent.Callable; + +@CommandLine.Command(name = "update", + description = "Update an existing policy", + synopsisHeading = "%n@|bold Usage:|@ ", + descriptionHeading = "%n", + optionListHeading = "%n@|bold Options:|@%n", + commandListHeading = "%n@|bold Commands:|@%n", + versionProvider = MqttCLIMain.CLIVersionProvider.class, + mixinStandardHelpOptions = true) +public class UpdatePolicyCommand implements Callable { + + @SuppressWarnings({"unused", "NotNullFieldNotInitialized"}) + @CommandLine.Option(names = {"-i", "--id"}, required = true, description = "The id of the policy") + private @NotNull String policyId; + + @SuppressWarnings({"NotNullFieldNotInitialized", "unused"}) + @CommandLine.ArgGroup(multiplicity = "1") + private @NotNull PolicyDefinitionOptions definitionOptions; + + @CommandLine.Mixin + private final @NotNull DataGovernanceOptions dataGovernanceOptions = new DataGovernanceOptions(); + + private final @NotNull OutputFormatter outputFormatter; + private final @NotNull HiveMQRestService hiveMQRestService; + private final @NotNull Gson gson; + + @Inject + public UpdatePolicyCommand( + final @NotNull HiveMQRestService hiveMQRestService, + final @NotNull OutputFormatter outputFormatter, + final @NotNull Gson gson) { + this.outputFormatter = outputFormatter; + this.hiveMQRestService = hiveMQRestService; + this.gson = gson; + } + + @Override + public @NotNull Integer call() { + Logger.trace("Command {}", this); + + final DataGovernanceHubPoliciesApi policiesApi = + hiveMQRestService.getPoliciesApi(dataGovernanceOptions.getUrl(), dataGovernanceOptions.getRateLimit()); + + if (policyId.isEmpty()) { + outputFormatter.printError("The policy id must not be empty."); + return 1; + } + + final String definition = definitionOptions.getDefinition(); + if (definition.isEmpty()) { + outputFormatter.printError("The policy definition must not be empty."); + return 1; + } + + final UpdatePolicyTask updatePolicyTask = + new UpdatePolicyTask(outputFormatter, policiesApi, gson, policyId, definition); + if (updatePolicyTask.execute()) { + return 0; + } else { + return 1; + } + } + + @Override + public @NotNull String toString() { + return "UpdatePolicyCommand{" + + "definitionOptions=" + + definitionOptions + + ", dataGovernanceOptions=" + + dataGovernanceOptions + + ", outputFormatter=" + + outputFormatter + + '}'; + } +} diff --git a/src/main/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTask.java b/src/main/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTask.java new file mode 100644 index 000000000..74714b220 --- /dev/null +++ b/src/main/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTask.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019-present HiveMQ and the HiveMQ Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.hivemq.cli.hivemq.policies; + +import com.google.gson.Gson; +import com.google.gson.JsonSyntaxException; +import com.hivemq.cli.commands.hivemq.datagovernance.OutputFormatter; +import com.hivemq.cli.openapi.ApiException; +import com.hivemq.cli.openapi.hivemq.DataGovernanceHubPoliciesApi; +import com.hivemq.cli.openapi.hivemq.Policy; +import org.jetbrains.annotations.NotNull; + +public class UpdatePolicyTask { + private final @NotNull OutputFormatter outputFormatter; + private final @NotNull DataGovernanceHubPoliciesApi policiesApi; + private final @NotNull Gson gson; + private final @NotNull String policyId; + private final @NotNull String definition; + + public UpdatePolicyTask( + final @NotNull OutputFormatter outputFormatter, + final @NotNull DataGovernanceHubPoliciesApi policiesApi, + final @NotNull Gson gson, + final @NotNull String policyId, + final @NotNull String definition) { + this.outputFormatter = outputFormatter; + this.policiesApi = policiesApi; + this.gson = gson; + this.policyId = policyId; + this.definition = definition; + } + + public boolean execute() { + final Policy policy; + try { + policy = gson.fromJson(definition, Policy.class); + } catch (final JsonSyntaxException jsonSyntaxException) { + outputFormatter.printError("Could not parse policy JSON: " + jsonSyntaxException.getMessage()); + return false; + } + + try { + policiesApi.updatePolicy(policyId, policy); + } catch (final ApiException apiException) { + outputFormatter.printApiException("Failed to update policy", apiException); + return false; + } + + return true; + } +} diff --git a/src/main/java/com/hivemq/cli/ioc/HiveMqModule.java b/src/main/java/com/hivemq/cli/ioc/HiveMqModule.java index 643f09e65..f579b8d39 100644 --- a/src/main/java/com/hivemq/cli/ioc/HiveMqModule.java +++ b/src/main/java/com/hivemq/cli/ioc/HiveMqModule.java @@ -26,6 +26,7 @@ import com.hivemq.cli.commands.hivemq.policies.GetPolicyCommand; import com.hivemq.cli.commands.hivemq.policies.ListPoliciesCommand; import com.hivemq.cli.commands.hivemq.policies.PoliciesCommand; +import com.hivemq.cli.commands.hivemq.policies.UpdatePolicyCommand; import com.hivemq.cli.commands.hivemq.schemas.CreateSchemaCommand; import com.hivemq.cli.commands.hivemq.schemas.DeleteSchemaCommand; import com.hivemq.cli.commands.hivemq.schemas.GetSchemaCommand; @@ -52,6 +53,7 @@ class HiveMqModule { final @NotNull ExportClientsCommand exportClientsCommand, final @NotNull PoliciesCommand policiesCommand, final @NotNull GetPolicyCommand getPolicyCommand, + final @NotNull UpdatePolicyCommand updatePolicyCommand, final @NotNull ListPoliciesCommand listPoliciesCommand, final @NotNull CreatePolicyCommand createPolicyCommand, final @NotNull DeletePolicyCommand deletePolicyCommand, @@ -64,6 +66,7 @@ class HiveMqModule { final @NotNull CommandErrorMessageHandler handler) { final CommandLine policiesCommandLine = new CommandLine(policiesCommand).addSubcommand(getPolicyCommand) + .addSubcommand(updatePolicyCommand) .addSubcommand(listPoliciesCommand) .addSubcommand(createPolicyCommand) .addSubcommand(deletePolicyCommand); diff --git a/src/test/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommandTest.java b/src/test/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommandTest.java new file mode 100644 index 000000000..6f3c62300 --- /dev/null +++ b/src/test/java/com/hivemq/cli/commands/hivemq/policies/UpdatePolicyCommandTest.java @@ -0,0 +1,116 @@ +/* + * Copyright 2019-present HiveMQ and the HiveMQ Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.hivemq.cli.commands.hivemq.policies; + +import com.google.gson.Gson; +import com.hivemq.cli.commands.hivemq.datagovernance.OutputFormatter; +import com.hivemq.cli.openapi.ApiException; +import com.hivemq.cli.openapi.hivemq.DataGovernanceHubPoliciesApi; +import com.hivemq.cli.rest.HiveMQRestService; +import com.hivemq.cli.utils.TestLoggerUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class UpdatePolicyCommandTest { + + private final @NotNull HiveMQRestService hiveMQRestService = mock(HiveMQRestService.class); + private final @NotNull Gson gson = new Gson(); + private final @NotNull OutputFormatter outputFormatter = mock(OutputFormatter.class); + private final @NotNull DataGovernanceHubPoliciesApi policiesApi = mock(DataGovernanceHubPoliciesApi.class); + + private final @NotNull CommandLine commandLine = + new CommandLine(new UpdatePolicyCommand(hiveMQRestService, outputFormatter, gson)); + + private static final @NotNull String POLICY_JSON = + "{ \"id\": \"policy-1\", \"matching\": { \"topicFilter\": \"a/#\" } }"; + + @BeforeEach + void setUp() { + TestLoggerUtils.resetLogger(); + when(hiveMQRestService.getPoliciesApi(any(), anyDouble())).thenReturn(policiesApi); + } + + @Test + void call_idMissing_error() { + assertEquals(2, commandLine.execute("--definition=" + POLICY_JSON)); + } + + @Test + void call_idEmpty_error() { + assertEquals(1, commandLine.execute("--id=", "--definition=" + POLICY_JSON)); + verify(outputFormatter).printError(eq("The policy id must not be empty.")); + } + + @Test + void call_definitionMissing_error() { + assertEquals(2, commandLine.execute("--id=policy-1")); + } + + @Test + void call_argumentDefinitionEmpty_error() { + assertEquals(1, commandLine.execute("--id=policy-1", "--definition=")); + verify(outputFormatter).printError(eq("The policy definition must not be empty.")); + } + + @Test + void call_fileDefinitionEmpty_error() throws IOException { + final File policyFile = File.createTempFile("policy", ".json"); + Files.write(policyFile.toPath(), "".getBytes()); + assertEquals(1, commandLine.execute("--id=policy-1", "--file=" + policyFile.getAbsolutePath())); + verify(outputFormatter).printError(eq("The policy definition must not be empty.")); + } + + @Test + void call_bothFileAndArgumentDefinition_error() throws IOException { + final File policyFile = File.createTempFile("policy", ".json"); + assertEquals(2, + commandLine.execute("--id=policy-1", "--definition='abc'", "--file=" + policyFile.getAbsolutePath())); + } + + @Test + void call_urlAndRateLimitPassed_usedInApi() { + assertEquals(0, + commandLine.execute("--rate=123", "--url=test-url", "--id=policy-1", "--definition=" + POLICY_JSON)); + verify(hiveMQRestService).getPoliciesApi(eq("test-url"), eq(123d)); + } + + @Test + void call_taskSuccessful_return0() throws ApiException { + assertEquals(0, commandLine.execute("--id=policy-1", "--definition=" + POLICY_JSON)); + verify(policiesApi).updatePolicy(eq("policy-1"), any()); + } + + @Test + void call_taskFailed_return1() throws ApiException { + when(policiesApi.updatePolicy(eq("policy-1"), any())).thenThrow(ApiException.class); + assertEquals(1, commandLine.execute("--id=policy-1", "--definition=" + POLICY_JSON)); + } +} diff --git a/src/test/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTaskTest.java b/src/test/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTaskTest.java new file mode 100644 index 000000000..1dc42ad3a --- /dev/null +++ b/src/test/java/com/hivemq/cli/hivemq/policies/UpdatePolicyTaskTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019-present HiveMQ and the HiveMQ Community + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * 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 com.hivemq.cli.hivemq.policies; + +import com.google.gson.Gson; +import com.hivemq.cli.commands.hivemq.datagovernance.OutputFormatter; +import com.hivemq.cli.openapi.ApiException; +import com.hivemq.cli.openapi.hivemq.DataGovernanceHubPoliciesApi; +import com.hivemq.cli.openapi.hivemq.Policy; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class UpdatePolicyTaskTest { + + private final @NotNull DataGovernanceHubPoliciesApi policiesApi = mock(DataGovernanceHubPoliciesApi.class); + private final @NotNull OutputFormatter outputFormatter = mock(OutputFormatter.class); + private final @NotNull ArgumentCaptor policyCaptor = ArgumentCaptor.forClass(Policy.class); + private final @NotNull Gson gson = new Gson(); + + private static final @NotNull String POLICY_ID = "policy-1"; + private static final @NotNull String POLICY_JSON = + "{ \"id\": \"" + POLICY_ID + "\", \"matching\": { \"topicFilter\": \"a/#\" } }"; + + @Test + void execute_validPolicy_success() throws ApiException { + final Policy policy = gson.fromJson(POLICY_JSON, Policy.class); + + final UpdatePolicyTask task = new UpdatePolicyTask(outputFormatter, policiesApi, gson, POLICY_ID, POLICY_JSON); + + when(policiesApi.updatePolicy(eq(POLICY_ID), policyCaptor.capture())).thenReturn(policy); + + assertTrue(task.execute()); + verify(policiesApi, times(1)).updatePolicy(eq(POLICY_ID), eq(policy)); + verify(outputFormatter, times(0)).printJson(any()); + assertEquals(policy, policyCaptor.getValue()); + } + + @Test + void execute_invalidDefinition_printError() throws ApiException { + final UpdatePolicyTask task = new UpdatePolicyTask(outputFormatter, policiesApi, gson, POLICY_ID, "invalid"); + + assertFalse(task.execute()); + verify(outputFormatter, times(1)).printError(any()); + verify(policiesApi, times(0)).updatePolicy(eq(POLICY_ID), any()); + } + + @Test + void execute_apiException_printError() throws ApiException { + final UpdatePolicyTask task = new UpdatePolicyTask(outputFormatter, policiesApi, gson, POLICY_ID, POLICY_JSON); + + when(policiesApi.updatePolicy(eq(POLICY_ID), any())).thenThrow(ApiException.class); + + assertFalse(task.execute()); + verify(outputFormatter, times(1)).printApiException(any(), any()); + verify(policiesApi, times(1)).updatePolicy(eq(POLICY_ID), any()); + } + +}