diff --git a/build.gradle b/build.gradle index f42239f..dcf7786 100644 --- a/build.gradle +++ b/build.gradle @@ -2,11 +2,12 @@ plugins { id 'java' id 'org.springframework.boot' version '3.1.3' id 'io.spring.dependency-management' version '1.1.3' + id 'jacoco' id 'maven-publish' } group = 'net.stelitop' -version = '0.0.2' +version = '0.0.3' java { sourceCompatibility = '17' @@ -34,6 +35,9 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'junit:junit:4.13.1' + testImplementation 'org.assertj:assertj-core:3.24.2' + testImplementation 'org.mockito:mockito-core:5.4.0' } publishing { @@ -56,4 +60,26 @@ publishing { tasks.named('test') { useJUnitPlatform() + jacoco { + enabled = true + includes = ['net.stelitop.mad4j.*'] + excludes = [] + } } + +jacocoTestCoverageVerification() { + dependsOn test + violationRules { + rule { + enabled = true + element = 'CLASS' + includes = ['net.stelitop.madj4.*'] + + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.1 + } + } + } +} \ No newline at end of file diff --git a/src/main/java/net/stelitop/mad4j/Mad4jConfig.java b/src/main/java/net/stelitop/mad4j/Mad4jConfig.java index dda5d77..927f582 100644 --- a/src/main/java/net/stelitop/mad4j/Mad4jConfig.java +++ b/src/main/java/net/stelitop/mad4j/Mad4jConfig.java @@ -1,7 +1,75 @@ package net.stelitop.mad4j; +import discord4j.core.GatewayDiscordClient; +import net.stelitop.mad4j.autocomplete.AutocompletionExecutor; +import net.stelitop.mad4j.listeners.CommandOptionAutocompleteListener; +import net.stelitop.mad4j.listeners.ComponentEventListener; +import net.stelitop.mad4j.listeners.SlashCommandListener; +import net.stelitop.mad4j.requirements.CommandRequirementExecutor; +import org.checkerframework.checker.units.qual.C; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; + +import java.util.List; @ComponentScan("net.stelitop.mad4j") +@Configuration public class Mad4jConfig { + } +//@Configuration +//public class Mad4jConfig { +// +// @Bean +// public SlashCommandRegistrar slashCommandRegistrar( +// GatewayDiscordClient gatewayDiscordClient, +// CommandOptionAutocompleteListener commandOptionAutocompleteListener, +// ApplicationContext applicationContext, +// Environment environment +// ) { +// return new SlashCommandRegistrar( +// gatewayDiscordClient, +// commandOptionAutocompleteListener, +// applicationContext, +// environment +// ); +// } +// +// @Bean +// public CommandOptionAutocompleteListener commandOptionAutocompleteListener( +// GatewayDiscordClient client, +// List autocompletionExecutors +// ) { +// return new CommandOptionAutocompleteListener( +// client, +// autocompletionExecutors +// ); +// } +// +// @Bean +// public ComponentEventListener componentEventListener( +// GatewayDiscordClient client, +// ApplicationContext applicationContext +// ) { +// return new ComponentEventListener( +// client, +// applicationContext +// ); +// } +// +// @Bean +// public SlashCommandListener slashCommandListener( +// ApplicationContext applicationContext, +// GatewayDiscordClient client, +// List possibleRequirements +// ) { +// return new SlashCommandListener( +// applicationContext, +// client, +// possibleRequirements +// ); +// } +//} diff --git a/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java b/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java index 450581c..2cb820f 100644 --- a/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java +++ b/src/main/java/net/stelitop/mad4j/SlashCommandRegistrar.java @@ -5,6 +5,7 @@ import discord4j.discordjson.json.ApplicationCommandOptionData; import discord4j.discordjson.json.ApplicationCommandRequest; import discord4j.discordjson.json.ImmutableApplicationCommandOptionData; +import discord4j.discordjson.possible.Possible; import discord4j.rest.RestClient; import lombok.AllArgsConstructor; import lombok.ToString; @@ -16,6 +17,7 @@ import net.stelitop.mad4j.utils.OptionType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.context.ApplicationContext; @@ -50,6 +52,7 @@ public class SlashCommandRegistrar implements ApplicationRunner { private final ApplicationContext applicationContext; private final Environment environment; + @Autowired public SlashCommandRegistrar( GatewayDiscordClient gatewayDiscordClient, CommandOptionAutocompleteListener commandOptionAutocompleteListener, @@ -292,6 +295,18 @@ private ApplicationCommandOptionData parseRegularCommandParam( String paramName = annotation.name().toLowerCase(); commandOptionAutocompleteListener.addMapping(commandName, paramName, annotation.autocomplete()); } + if (annotation.minValue() != Double.MIN_VALUE) { + acodBuilder.minValue(annotation.minValue()); + } + if (annotation.maxValue() != Double.MAX_VALUE) { + acodBuilder.maxValue(annotation.maxValue()); + } + if (annotation.minLength() > 0) { + acodBuilder.minLength(annotation.minLength()); + } + if (annotation.maxLength() != Integer.MAX_VALUE) { + acodBuilder.maxLength(annotation.maxLength()); + } addChoicesToCommandParam(acodBuilder, annotation.choices()); return acodBuilder.build(); diff --git a/src/main/java/net/stelitop/mad4j/commands/CommandParam.java b/src/main/java/net/stelitop/mad4j/commands/CommandParam.java index f9d60ac..5ca0436 100644 --- a/src/main/java/net/stelitop/mad4j/commands/CommandParam.java +++ b/src/main/java/net/stelitop/mad4j/commands/CommandParam.java @@ -41,4 +41,22 @@ */ Class autocomplete() default NullAutocompleteExecutor.class; + /** + * The minimum value that this parameter must take. Only works for numerical params. + */ + double minValue() default Double.MIN_VALUE; + /** + * The maximum value that this parameter must take. Only works for numerical params. + */ + double maxValue() default Double.MAX_VALUE; + + /** + * The minimum length for a string parameter. + */ + int minLength() default 0; + + /** + * The maximum length for a string parameter. + */ + int maxLength() default Integer.MAX_VALUE; } \ No newline at end of file diff --git a/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java b/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java index 1bed60f..387d4de 100644 --- a/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java +++ b/src/main/java/net/stelitop/mad4j/listeners/CommandOptionAutocompleteListener.java @@ -10,6 +10,7 @@ import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.stereotype.Component; @@ -30,6 +31,7 @@ public class CommandOptionAutocompleteListener implements ApplicationRunner { private final Map, AutocompletionExecutor> autocompletionExecutorBeans; private final Map, Class> commandNameParamToExecutor; + @Autowired public CommandOptionAutocompleteListener( GatewayDiscordClient client, List autocompletionExecutors diff --git a/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java b/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java index 5cec967..cd8cae1 100644 --- a/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java +++ b/src/main/java/net/stelitop/mad4j/listeners/ComponentEventListener.java @@ -18,6 +18,7 @@ import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.context.ApplicationContext; @@ -38,6 +39,7 @@ public class ComponentEventListener implements ApplicationRunner { private final GatewayDiscordClient client; private final ApplicationContext applicationContext; + @Autowired public ComponentEventListener( GatewayDiscordClient client, ApplicationContext applicationContext diff --git a/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java b/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java index 1fd67f5..03a2e0d 100644 --- a/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java +++ b/src/main/java/net/stelitop/mad4j/listeners/SlashCommandListener.java @@ -18,6 +18,7 @@ import net.stelitop.mad4j.requirements.CommandRequirement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.context.ApplicationContext; @@ -55,6 +56,7 @@ public class SlashCommandListener implements ApplicationRunner { private final Map, CommandRequirementExecutor> possibleRequirements; private List slashCommands; + @Autowired public SlashCommandListener( ApplicationContext applicationContext, GatewayDiscordClient client, diff --git a/src/test/java/slashcommands/registering/BaseTestConfiguration.java b/src/test/java/slashcommands/registering/BaseTestConfiguration.java new file mode 100644 index 0000000..9e78e9a --- /dev/null +++ b/src/test/java/slashcommands/registering/BaseTestConfiguration.java @@ -0,0 +1,45 @@ +package slashcommands.registering; + +import discord4j.core.GatewayDiscordClient; +import discord4j.rest.RestClient; +import discord4j.rest.service.ApplicationService; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SpringBootConfiguration +@ComponentScan("net.stelitop.mad4j") +public class BaseTestConfiguration { + + public static long TEST_APPLICATION_ID = 1L; + + @Bean + public GatewayDiscordClient gatewayDiscordClient(RestClient restClientMock) { + GatewayDiscordClient gatewayDiscordClientMock = mock(GatewayDiscordClient.class); + when(gatewayDiscordClientMock.getRestClient()).thenReturn(restClientMock); + when(gatewayDiscordClientMock.on(any(), any())).thenReturn(Flux.empty()); + return gatewayDiscordClientMock; + } + + @Bean + public RestClient restClient(ApplicationService applicationServiceMock) { + RestClient restClientMock = mock(RestClient.class); + when(restClientMock.getApplicationId()).thenReturn(Mono.just(TEST_APPLICATION_ID)); + when(restClientMock.getApplicationService()).thenReturn(applicationServiceMock); + return restClientMock; + } + + @Bean + ApplicationService applicationService() { + ApplicationService applicationServiceMock = mock(ApplicationService.class); + when(applicationServiceMock.bulkOverwriteGlobalApplicationCommand(eq(TEST_APPLICATION_ID), any())).thenReturn(Flux.empty()); + return applicationServiceMock; + } +} \ No newline at end of file diff --git a/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java b/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java new file mode 100644 index 0000000..172500b --- /dev/null +++ b/src/test/java/slashcommands/registering/BaseTestConfigurationTest.java @@ -0,0 +1,18 @@ +package slashcommands.registering; + +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Import(BaseTestConfiguration.class) +public class BaseTestConfigurationTest { + + @Test + void testSpringTestContext() { + + } +} diff --git a/src/test/java/slashcommands/registering/CommandGroupTest.java b/src/test/java/slashcommands/registering/CommandGroupTest.java new file mode 100644 index 0000000..b729d6e --- /dev/null +++ b/src/test/java/slashcommands/registering/CommandGroupTest.java @@ -0,0 +1,73 @@ +package slashcommands.registering; + +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.discordjson.json.ApplicationCommandOptionData; +import discord4j.discordjson.json.ApplicationCommandRequest; +import discord4j.rest.service.ApplicationService; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.CommandParam; +import net.stelitop.mad4j.commands.SlashCommand; +import net.stelitop.mad4j.utils.OptionType; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Mono; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Import({BaseTestConfiguration.class, CommandGroupTest.TestComponent.class}) +public class CommandGroupTest { + + @Autowired + private ApplicationService applicationServiceMock; + + @DiscordEventsComponent + public static class TestComponent { + + static final String commandDescription = "Adds two numbers."; + static final String param1Description = "The first number."; + static final String param2Description = "The second number."; + + @SlashCommand(name = "add numbers", description = commandDescription) + public Mono addTwoNumbersCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "x", description = param1Description) long x, + @CommandParam(name = "y", description = param2Description) long y + ) { + return event.reply("The sum is " + x + y); + } + } + + @Test + public void testLoadingCommand() { + ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); + verify(applicationServiceMock, times(1)).bulkOverwriteGlobalApplicationCommand( + eq(BaseTestConfiguration.TEST_APPLICATION_ID), argumentCaptor.capture()); + + assertThat(argumentCaptor.getValue()).hasSize(1); + ApplicationCommandRequest request = argumentCaptor.getValue().get(0); + assertThat(request.name()).isEqualTo("add"); + assertThat(request.options().get()).hasSize(1); + assertThat(request.options().get().get(0).type()).isEqualTo(OptionType.SUB_COMMAND); + assertThat(request.options().get().get(0).name()).isEqualTo("numbers"); + assertThat(request.options().get().get(0).description()).isEqualTo(TestComponent.commandDescription); + assertThat(request.options().get().get(0).options().get()).containsExactly( + ApplicationCommandOptionData.builder().name("x").type(OptionType.INTEGER).required(true) + .description(TestComponent.param1Description).build(), + ApplicationCommandOptionData.builder().name("y").type(OptionType.INTEGER).required(true) + .description(TestComponent.param2Description).build() + ); + } +} diff --git a/src/test/java/slashcommands/registering/PlainCommandTest.java b/src/test/java/slashcommands/registering/PlainCommandTest.java new file mode 100644 index 0000000..55dfa69 --- /dev/null +++ b/src/test/java/slashcommands/registering/PlainCommandTest.java @@ -0,0 +1,70 @@ +package slashcommands.registering; + +import discord4j.core.GatewayDiscordClient; +import discord4j.core.event.domain.interaction.ChatInputInteractionEvent; +import discord4j.discordjson.json.ApplicationCommandOptionData; +import discord4j.discordjson.json.ApplicationCommandRequest; +import discord4j.rest.RestClient; +import discord4j.rest.service.ApplicationService; +import net.stelitop.mad4j.DiscordEventsComponent; +import net.stelitop.mad4j.InteractionEvent; +import net.stelitop.mad4j.commands.CommandParam; +import net.stelitop.mad4j.commands.SlashCommand; +import net.stelitop.mad4j.utils.OptionType; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.junit4.SpringRunner; +import reactor.core.publisher.Mono; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@RunWith(SpringRunner.class) +@SpringBootTest +@Import({BaseTestConfiguration.class, PlainCommandTest.TestComponent.class}) +public class PlainCommandTest { + + @Autowired + private ApplicationService applicationServiceMock; + + @DiscordEventsComponent + public static class TestComponent { + + static final String commandDescription = "Adds two numbers."; + static final String param1Description = "The first number."; + static final String param2Description = "The second number."; + + @SlashCommand(name = "add", description = commandDescription) + public Mono addTwoNumbersCommand( + @InteractionEvent ChatInputInteractionEvent event, + @CommandParam(name = "x", description = param1Description) long x, + @CommandParam(name = "y", description = param2Description) long y + ) { + return event.reply("The sum is " + x + y); + } + } + + @Test + public void testLoadingCommand() { + ArgumentCaptor> argumentCaptor = ArgumentCaptor.forClass(List.class); + verify(applicationServiceMock, times(1)).bulkOverwriteGlobalApplicationCommand( + eq(BaseTestConfiguration.TEST_APPLICATION_ID), argumentCaptor.capture()); + + assertThat(argumentCaptor.getValue()).hasSize(1); + ApplicationCommandRequest request = argumentCaptor.getValue().get(0); + assertThat(request.name()).isEqualTo("add"); + assertThat(request.description().get()).isEqualTo(TestComponent.commandDescription); + assertThat(request.options().get()).containsExactly( + ApplicationCommandOptionData.builder().name("x").type(OptionType.INTEGER).required(true) + .description(TestComponent.param1Description).build(), + ApplicationCommandOptionData.builder().name("y").type(OptionType.INTEGER).required(true) + .description(TestComponent.param2Description).build() + ); + } +}