From ea8453176a2a36d4bd428a2f977f56eedc23d23e Mon Sep 17 00:00:00 2001 From: Jun Nemoto Date: Wed, 3 Dec 2025 17:26:48 +0900 Subject: [PATCH] Add create-namespace command --- client/build.gradle | 8 +- .../dl/client/tool/NamespaceCreation.java | 29 ++++ .../dl/client/tool/ScalarDlCommandLine.java | 3 + .../dl/client/tool/NamespaceCreationTest.java | 153 ++++++++++++++++++ .../client/tool/ScalarDlCommandLineTest.java | 4 + 5 files changed, 193 insertions(+), 4 deletions(-) create mode 100644 client/src/main/java/com/scalar/dl/client/tool/NamespaceCreation.java create mode 100644 client/src/test/java/com/scalar/dl/client/tool/NamespaceCreationTest.java diff --git a/client/build.gradle b/client/build.gradle index 3f4954f0..2081b344 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -124,9 +124,9 @@ task LedgerValidation(type: CreateStartScripts) { classpath = jar.outputs.files + project.configurations.runtimeClasspath } -task MultiLedgersValidation(type: CreateStartScripts) { - mainClass = 'com.scalar.dl.client.tool.MultiLedgersValidation' - applicationName = 'validate-ledgers' +task NamespaceCreation(type: CreateStartScripts) { + mainClass = 'com.scalar.dl.client.tool.NamespaceCreation' + applicationName = 'create-namespace' outputDir = new File(project.buildDir, 'tmp') classpath = jar.outputs.files + project.configurations.runtimeClasspath } @@ -169,7 +169,7 @@ applicationDistribution.into('bin') { from(ContractsListing) from(ContractExecution) from(LedgerValidation) - from(MultiLedgersValidation) + from(NamespaceCreation) from(ScalarDl) from(ScalarDlGc) from(StateUpdaterSimpleBench) diff --git a/client/src/main/java/com/scalar/dl/client/tool/NamespaceCreation.java b/client/src/main/java/com/scalar/dl/client/tool/NamespaceCreation.java new file mode 100644 index 00000000..5e6d148a --- /dev/null +++ b/client/src/main/java/com/scalar/dl/client/tool/NamespaceCreation.java @@ -0,0 +1,29 @@ +package com.scalar.dl.client.tool; + +import com.scalar.dl.client.exception.ClientException; +import com.scalar.dl.client.service.ClientService; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "create-namespace", description = "Create a namespace.") +public class NamespaceCreation extends AbstractClientCommand { + + @CommandLine.Option( + names = {"--namespace"}, + required = true, + paramLabel = "NAMESPACE", + description = "A namespace name to create.") + private String namespace; + + public static void main(String[] args) { + int exitCode = new CommandLine(new NamespaceCreation()).execute(args); + System.exit(exitCode); + } + + @Override + protected Integer execute(ClientService service) throws ClientException { + service.createNamespace(namespace); + Common.printOutput(null); + return 0; + } +} diff --git a/client/src/main/java/com/scalar/dl/client/tool/ScalarDlCommandLine.java b/client/src/main/java/com/scalar/dl/client/tool/ScalarDlCommandLine.java index d6d98f9a..ffec1b9b 100644 --- a/client/src/main/java/com/scalar/dl/client/tool/ScalarDlCommandLine.java +++ b/client/src/main/java/com/scalar/dl/client/tool/ScalarDlCommandLine.java @@ -27,6 +27,7 @@ HelpCommand.class, LedgerValidation.class, SecretRegistration.class, + NamespaceCreation.class, }, description = {"These are ScalarDL commands used in various situations:"}) public class ScalarDlCommandLine { @@ -77,6 +78,8 @@ static void setupSections(CommandLine cmd) { sections.put( "%nexecute and list the registered business logic%n", Arrays.asList(ContractExecution.class, ContractsListing.class)); + // Section: manage namespaces. + sections.put("%nmanage namespaces%n", Collections.singletonList(NamespaceCreation.class)); // Section: validate ledger. sections.put("%nvalidate ledger%n", Collections.singletonList(LedgerValidation.class)); // Section: generic contracts. diff --git a/client/src/test/java/com/scalar/dl/client/tool/NamespaceCreationTest.java b/client/src/test/java/com/scalar/dl/client/tool/NamespaceCreationTest.java new file mode 100644 index 00000000..ad7c6a3f --- /dev/null +++ b/client/src/test/java/com/scalar/dl/client/tool/NamespaceCreationTest.java @@ -0,0 +1,153 @@ +package com.scalar.dl.client.tool; + +import static com.scalar.dl.client.tool.CommandLineTestUtils.createDefaultClientPropertiesFile; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.scalar.dl.client.config.ClientConfig; +import com.scalar.dl.client.config.GatewayClientConfig; +import com.scalar.dl.client.exception.ClientException; +import com.scalar.dl.client.service.ClientService; +import com.scalar.dl.client.service.ClientServiceFactory; +import com.scalar.dl.ledger.service.StatusCode; +import java.io.File; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import picocli.CommandLine; + +public class NamespaceCreationTest { + private CommandLine commandLine; + + @BeforeEach + void setup() { + commandLine = new CommandLine(new NamespaceCreation()); + } + + @Nested + @DisplayName("#call()") + class call { + @Test + @DisplayName("returns 0 as exit code") + void returns0AsExitCode() throws ClientException { + // Arrange + String[] args = + new String[] { + // Set the required options. + "--properties=PROPERTIES_FILE", "--namespace=test_namespace", + }; + NamespaceCreation command = parseArgs(args); + ClientService serviceMock = mock(ClientService.class); + + // Act + int exitCode = command.execute(serviceMock); + + // Assert + assertThat(exitCode).isEqualTo(0); + verify(serviceMock).createNamespace("test_namespace"); + } + + @Nested + @DisplayName("where useGateway option is true") + class whereUseGatewayOptionIsTrue { + @Test + @DisplayName("create ClientService with GatewayClientConfig") + public void createClientServiceWithGatewayClientConfig(@TempDir Path tempDir) + throws Exception { + // Arrange + File file = createDefaultClientPropertiesFile(tempDir, "client.props"); + String propertiesOption = String.format("--properties=%s", file.getAbsolutePath()); + String[] args = + new String[] { + // Set the required options. + propertiesOption, + "--namespace=test-namespace", + // Enable Gateway. + "--use-gateway" + }; + NamespaceCreation command = parseArgs(args); + ClientServiceFactory factory = mock(ClientServiceFactory.class); + doReturn(mock(ClientService.class)).when(factory).create(any(GatewayClientConfig.class)); + + // Act + command.call(factory); + + // Verify + verify(factory).create(any(GatewayClientConfig.class)); + verify(factory, never()).create(any(ClientConfig.class)); + } + } + + @Nested + @DisplayName("where useGateway option is false") + class whereUseGatewayOptionIsFalse { + @Test + @DisplayName("create ClientService with ClientConfig") + public void createClientServiceWithClientConfig(@TempDir Path tempDir) throws Exception { + // Arrange + File file = createDefaultClientPropertiesFile(tempDir, "client.props"); + String propertiesOption = String.format("--properties=%s", file.getAbsolutePath()); + String[] args = + new String[] { + // Set the required options. + propertiesOption, "--namespace=test-namespace", + // Gateway is disabled by default. + }; + NamespaceCreation command = parseArgs(args); + ClientServiceFactory factory = mock(ClientServiceFactory.class); + doReturn(mock(ClientService.class)).when(factory).create(any(ClientConfig.class)); + + // Act + command.call(factory); + + // Verify + verify(factory).create(any(ClientConfig.class)); + verify(factory, never()).create(any(GatewayClientConfig.class)); + } + } + + @Nested + @DisplayName("where ClientService throws ClientException") + class whereClientExceptionIsThrownByClientService { + @Test + @DisplayName("returns 1 as exit code") + void returns1AsExitCode(@TempDir Path tempDir) throws Exception { + // Arrange + File file = createDefaultClientPropertiesFile(tempDir, "client.props"); + String[] args = + new String[] { + // Set the required options. + "--properties=" + file.getAbsolutePath(), "--namespace=test-namespace", + }; + NamespaceCreation command = parseArgs(args); + // Mock service that throws an exception. + ClientServiceFactory factoryMock = mock(ClientServiceFactory.class); + ClientService serviceMock = mock(ClientService.class); + when(factoryMock.create(any(ClientConfig.class))).thenReturn(serviceMock); + doThrow(new ClientException("", StatusCode.RUNTIME_ERROR)) + .when(serviceMock) + .createNamespace("test-namespace"); + + // Act + int exitCode = command.call(factoryMock); + + // Assert + assertThat(exitCode).isEqualTo(1); + verify(factoryMock).close(); + } + } + } + + private NamespaceCreation parseArgs(String[] args) { + return CommandLineTestUtils.parseArgs(commandLine, NamespaceCreation.class, args); + } +} diff --git a/client/src/test/java/com/scalar/dl/client/tool/ScalarDlCommandLineTest.java b/client/src/test/java/com/scalar/dl/client/tool/ScalarDlCommandLineTest.java index 86701205..44db1383 100644 --- a/client/src/test/java/com/scalar/dl/client/tool/ScalarDlCommandLineTest.java +++ b/client/src/test/java/com/scalar/dl/client/tool/ScalarDlCommandLineTest.java @@ -57,6 +57,9 @@ void displaysGroupedSubcommands() { " execute-contract Execute a specified contract.", " list-contracts List registered contracts.", "", + "manage namespaces", + " create-namespace Create a namespace.", + "", "validate ledger", " validate-ledger Validate a specified asset in a ledger.", "", @@ -97,6 +100,7 @@ void memberValuesAreProperlySet() { CommandLine.HelpCommand.class, LedgerValidation.class, SecretRegistration.class, + NamespaceCreation.class, }); } }