From 4ef9b7efbc6fa9b9d70a2222f62eab8c3431e40b Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:50:10 +0700 Subject: [PATCH] feat: add tests --- .../java/org/discordbots/api/DBLAPITest.java | 69 ++++++++++++ .../org/discordbots/api/DBLWidgetTest.java | 31 ++++++ .../api/interceptors/BaseInterceptor.java | 60 ++++++++++ .../api/interceptors/GetSelfInterceptor.java | 20 ++++ .../api/interceptors/GetVoteInterceptor.java | 22 ++++ .../api/interceptors/GetVotesInterceptor.java | 22 ++++ .../interceptors/PostCommandsInterceptor.java | 20 ++++ .../webhooks/DBLWebhooksTestSuite.java | 15 +++ .../java/org/discordbots/webhooks/Mocks.java | 47 ++++++++ .../webhooks/dropwizard/CustomServer.java | 16 +++ .../webhooks/dropwizard/CustomWebhooks.java | 35 ++++++ .../dropwizard/DBLDropwizardWebhooksTest.java | 68 ++++++++++++ .../webhooks/eclipsejetty/CustomWebhooks.java | 51 +++++++++ .../DBLEclipseJettyWebhooksTest.java | 105 ++++++++++++++++++ .../webhooks/springboot/CustomServer.java | 11 ++ .../CustomServerSecurityConfiguration.java | 16 +++ .../webhooks/springboot/CustomWebhooks.java | 49 ++++++++ .../springboot/DBLSpringBootWebhooksTest.java | 59 ++++++++++ src/test/resources/GetSelfResponse.json | 16 +++ src/test/resources/GetVoteResponse.json | 5 + src/test/resources/GetVotesResponse.json | 33 ++++++ .../resources/IntegrationCreatePayload.json | 19 ++++ .../resources/IntegrationDeletePayload.json | 6 + src/test/resources/LeadResponse.json | 3 + src/test/resources/PostCommands.json | 11 ++ src/test/resources/TestPayload.json | 17 +++ src/test/resources/VoteCreatePayload.json | 21 ++++ src/test/resources/dropwizard-test-config.yml | 7 ++ 28 files changed, 854 insertions(+) create mode 100644 src/test/java/org/discordbots/api/DBLAPITest.java create mode 100644 src/test/java/org/discordbots/api/DBLWidgetTest.java create mode 100644 src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java create mode 100644 src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java create mode 100644 src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java create mode 100644 src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java create mode 100644 src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java create mode 100644 src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java create mode 100644 src/test/java/org/discordbots/webhooks/Mocks.java create mode 100644 src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java create mode 100644 src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java create mode 100644 src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java create mode 100644 src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java create mode 100644 src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java create mode 100644 src/test/java/org/discordbots/webhooks/springboot/CustomServer.java create mode 100644 src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java create mode 100644 src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java create mode 100644 src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java create mode 100644 src/test/resources/GetSelfResponse.json create mode 100644 src/test/resources/GetVoteResponse.json create mode 100644 src/test/resources/GetVotesResponse.json create mode 100644 src/test/resources/IntegrationCreatePayload.json create mode 100644 src/test/resources/IntegrationDeletePayload.json create mode 100644 src/test/resources/LeadResponse.json create mode 100644 src/test/resources/PostCommands.json create mode 100644 src/test/resources/TestPayload.json create mode 100644 src/test/resources/VoteCreatePayload.json create mode 100644 src/test/resources/dropwizard-test-config.yml diff --git a/src/test/java/org/discordbots/api/DBLAPITest.java b/src/test/java/org/discordbots/api/DBLAPITest.java new file mode 100644 index 0000000..99fb651 --- /dev/null +++ b/src/test/java/org/discordbots/api/DBLAPITest.java @@ -0,0 +1,69 @@ +package org.discordbots.api; + +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import okhttp3.OkHttpClient; +import org.discordbots.api.entity.PaginatedVotes; +import org.discordbots.api.entity.UserSource; +import org.discordbots.api.interceptors.GetSelfInterceptor; +import org.discordbots.api.interceptors.GetVoteInterceptor; +import org.discordbots.api.interceptors.GetVotesInterceptor; +import org.discordbots.api.interceptors.PostCommandsInterceptor; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class DBLAPITest { + private static DBLAPI CLIENT; + + @BeforeAll + public static void setup() { + CLIENT = + new DBLAPI( + new OkHttpClient.Builder() + .addInterceptor(new GetSelfInterceptor()) + .addInterceptor(new GetVoteInterceptor()) + .addInterceptor(new GetVotesInterceptor()) + .addInterceptor(new PostCommandsInterceptor()) + .build()); + } + + @Test + public void getSelf() { + CLIENT.getSelf().toCompletableFuture().join(); + } + + @Test + public void postCommands() { + final JsonArray commands = + JsonParser.parseReader( + new InputStreamReader( + getClass().getClassLoader().getResourceAsStream("PostCommands.json"), + StandardCharsets.UTF_8)) + .getAsJsonArray(); + + CLIENT.postCommands(commands).toCompletableFuture().join(); + } + + @ParameterizedTest + @EnumSource(UserSource.class) + public void getVote(final UserSource userSource) { + CLIENT.getVote(userSource, "123456").toCompletableFuture().join(); + } + + @Test + @SuppressWarnings("unused") + public void getVotes() { + final PaginatedVotes firstPage = + CLIENT + .getVotes(OffsetDateTime.of(2026, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)) + .toCompletableFuture() + .join(); + final PaginatedVotes secondPage = firstPage.next().toCompletableFuture().join(); + } +} diff --git a/src/test/java/org/discordbots/api/DBLWidgetTest.java b/src/test/java/org/discordbots/api/DBLWidgetTest.java new file mode 100644 index 0000000..1d8780f --- /dev/null +++ b/src/test/java/org/discordbots/api/DBLWidgetTest.java @@ -0,0 +1,31 @@ +package org.discordbots.api; + +import org.discordbots.api.entity.ProjectType; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +public class DBLWidgetTest { + @ParameterizedTest + @EnumSource(ProjectType.class) + public void large(final ProjectType projectType) { + DBLWidget.large(projectType, "123456"); + } + + @ParameterizedTest + @EnumSource(ProjectType.class) + public void votes(final ProjectType projectType) { + DBLWidget.votes(projectType, "123456"); + } + + @ParameterizedTest + @EnumSource(ProjectType.class) + public void owner(final ProjectType projectType) { + DBLWidget.owner(projectType, "123456"); + } + + @ParameterizedTest + @EnumSource(ProjectType.class) + public void social(final ProjectType projectType) { + DBLWidget.social(projectType, "123456"); + } +} diff --git a/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java b/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java new file mode 100644 index 0000000..63147a6 --- /dev/null +++ b/src/test/java/org/discordbots/api/interceptors/BaseInterceptor.java @@ -0,0 +1,60 @@ +package org.discordbots.api.interceptors; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +public abstract class BaseInterceptor implements Interceptor { + @SuppressWarnings("FieldMayBeFinal") + private String response; + + public BaseInterceptor() { + try { + final String className = getClass().getSimpleName(); + + final InputStream inputStream = + BaseInterceptor.class.getResourceAsStream( + "/" + className.substring(0, className.length() - 11) + "Response.json"); + + response = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } catch (final IOException | NullPointerException ignored) { + response = ""; + } + } + + protected abstract boolean isCorrect(final String method, final String path, final HttpUrl url); + + protected abstract int getStatusCode(); + + protected abstract String getMessage(); + + @Override + public Response intercept(final Chain chain) throws IOException { + final Request request = chain.request(); + + final HttpUrl url = request.url(); + final String path = String.join("/", url.pathSegments()); + + if (url.host().equals("top.gg") + && path.startsWith("api/v1") + && isCorrect(request.method(), path, url)) { + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(getStatusCode()) + .message(getMessage()) + .body(ResponseBody.create(response, MediaType.get("application/json"))) + .addHeader("content-type", "application/json") + .build(); + } + + return chain.proceed(request); + } +} diff --git a/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java new file mode 100644 index 0000000..33cc308 --- /dev/null +++ b/src/test/java/org/discordbots/api/interceptors/GetSelfInterceptor.java @@ -0,0 +1,20 @@ +package org.discordbots.api.interceptors; + +import okhttp3.HttpUrl; + +public class GetSelfInterceptor extends BaseInterceptor { + @Override + protected boolean isCorrect(final String method, final String path, final HttpUrl url) { + return method.equals("GET") && path.endsWith("/projects/@me"); + } + + @Override + protected int getStatusCode() { + return 200; + } + + @Override + protected String getMessage() { + return "OK"; + } +} diff --git a/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java new file mode 100644 index 0000000..dd99906 --- /dev/null +++ b/src/test/java/org/discordbots/api/interceptors/GetVoteInterceptor.java @@ -0,0 +1,22 @@ +package org.discordbots.api.interceptors; + +import okhttp3.HttpUrl; + +public class GetVoteInterceptor extends BaseInterceptor { + @Override + protected boolean isCorrect(final String method, final String path, final HttpUrl url) { + return method.equals("GET") + && path.contains("/projects/@me/votes/") + && url.queryParameter("source") != null; + } + + @Override + protected int getStatusCode() { + return 200; + } + + @Override + protected String getMessage() { + return "OK"; + } +} diff --git a/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java b/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java new file mode 100644 index 0000000..829329b --- /dev/null +++ b/src/test/java/org/discordbots/api/interceptors/GetVotesInterceptor.java @@ -0,0 +1,22 @@ +package org.discordbots.api.interceptors; + +import okhttp3.HttpUrl; + +public class GetVotesInterceptor extends BaseInterceptor { + @Override + protected boolean isCorrect(final String method, final String path, final HttpUrl url) { + return method.equals("GET") + && path.endsWith("/projects/@me/votes") + && (url.queryParameter("startDate") != null || url.queryParameter("cursor") != null); + } + + @Override + protected int getStatusCode() { + return 200; + } + + @Override + protected String getMessage() { + return "OK"; + } +} diff --git a/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java b/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java new file mode 100644 index 0000000..9ff895e --- /dev/null +++ b/src/test/java/org/discordbots/api/interceptors/PostCommandsInterceptor.java @@ -0,0 +1,20 @@ +package org.discordbots.api.interceptors; + +import okhttp3.HttpUrl; + +public class PostCommandsInterceptor extends BaseInterceptor { + @Override + protected boolean isCorrect(final String method, final String path, final HttpUrl url) { + return method.equals("POST") && path.endsWith("/projects/@me/commands"); + } + + @Override + protected int getStatusCode() { + return 204; + } + + @Override + protected String getMessage() { + return "No Content"; + } +} diff --git a/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java b/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java new file mode 100644 index 0000000..66f58ae --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/DBLWebhooksTestSuite.java @@ -0,0 +1,15 @@ +package org.discordbots.webhooks; + +import org.discordbots.webhooks.dropwizard.DBLDropwizardWebhooksTest; +import org.discordbots.webhooks.eclipsejetty.DBLEclipseJettyWebhooksTest; +import org.discordbots.webhooks.springboot.DBLSpringBootWebhooksTest; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({ + DBLDropwizardWebhooksTest.class, + DBLEclipseJettyWebhooksTest.class, + DBLSpringBootWebhooksTest.class +}) +public class DBLWebhooksTestSuite {} diff --git a/src/test/java/org/discordbots/webhooks/Mocks.java b/src/test/java/org/discordbots/webhooks/Mocks.java new file mode 100644 index 0000000..c471d13 --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/Mocks.java @@ -0,0 +1,47 @@ +package org.discordbots.webhooks; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.Instant; +import java.util.HexFormat; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +public class Mocks { + public final String integrationCreatePayload; + public final String integrationDeletePayload; + public final String testPayload; + public final String voteCreatePayload; + + public Mocks() throws IOException, NullPointerException { + integrationCreatePayload = read("IntegrationCreate"); + integrationDeletePayload = read("IntegrationDelete"); + testPayload = read("Test"); + voteCreatePayload = read("VoteCreate"); + } + + private static String read(final String name) throws IOException, NullPointerException { + final InputStream inputStream = Mocks.class.getResourceAsStream("/" + name + "Payload.json"); + + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + + public static String signature(final String secret, final String body) + throws NoSuchAlgorithmException, InvalidKeyException { + final long timestamp = Instant.now().getEpochSecond(); + + final SecretKeySpec key = + new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"); + final Mac hmac = Mac.getInstance("HmacSHA256"); + + hmac.init(key); + + final byte[] digest = + hmac.doFinal(String.format("%s.%s", timestamp, body).getBytes(StandardCharsets.UTF_8)); + + return "t=" + Long.toString(timestamp) + ",v1=" + HexFormat.of().formatHex(digest); + } +} diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java b/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java new file mode 100644 index 0000000..0885b6c --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/dropwizard/CustomServer.java @@ -0,0 +1,16 @@ +package org.discordbots.webhooks.dropwizard; + +import io.dropwizard.core.Application; +import io.dropwizard.core.Configuration; +import io.dropwizard.core.setup.Environment; + +public class CustomServer extends Application { + public static void main(final String[] args) throws Exception { + new CustomServer().run(args); + } + + @Override + public void run(final Configuration config, final Environment env) { + env.jersey().register(new CustomWebhooks()); + } +} diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java new file mode 100644 index 0000000..246abd8 --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/dropwizard/CustomWebhooks.java @@ -0,0 +1,35 @@ +package org.discordbots.webhooks.dropwizard; + +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.Response; +import org.discordbots.webhooks.payload.IntegrationCreatePayload; +import org.discordbots.webhooks.payload.IntegrationDeletePayload; +import org.discordbots.webhooks.payload.TestPayload; +import org.discordbots.webhooks.payload.VoteCreatePayload; + +@Path("/webhook") +public class CustomWebhooks extends DBLWebhooks { + public CustomWebhooks() { + super(System.getenv("TOPGG_WEBHOOK_SECRET")); + } + + @Override + public Response onIntegrationCreate(final IntegrationCreatePayload payload, final String trace) { + return Response.status(Response.Status.OK).entity("dw:integrationCreate," + trace).build(); + } + + @Override + public Response onIntegrationDelete(final IntegrationDeletePayload payload, final String trace) { + return Response.status(Response.Status.OK).entity("dw:integrationDelete," + trace).build(); + } + + @Override + public Response onTest(final TestPayload payload, final String trace) { + return Response.status(Response.Status.OK).entity("dw:test," + trace).build(); + } + + @Override + public Response onVoteCreate(final VoteCreatePayload payload, final String trace) { + return Response.status(Response.Status.OK).entity("dw:voteCreate," + trace).build(); + } +} diff --git a/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java b/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java new file mode 100644 index 0000000..0456eee --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/dropwizard/DBLDropwizardWebhooksTest.java @@ -0,0 +1,68 @@ +package org.discordbots.webhooks.dropwizard; + +import io.dropwizard.core.Configuration; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit5.DropwizardAppExtension; +import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import org.discordbots.webhooks.Mocks; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(DropwizardExtensionsSupport.class) +public class DBLDropwizardWebhooksTest { + private static final DropwizardAppExtension APP = + new DropwizardAppExtension<>( + CustomServer.class, ResourceHelpers.resourceFilePath("dropwizard-test-config.yml")); + + private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET"); + private static final String TRACE = "trace"; + private static Mocks MOCKS; + + @BeforeAll + public static void setup() throws IOException, NullPointerException { + MOCKS = new Mocks(); + } + + private void send(final String name, final String payload) + throws NoSuchAlgorithmException, InvalidKeyException { + final Response response = + APP.client() + .target(String.format("http://localhost:%d/webhook", APP.getLocalPort())) + .request() + .header("Content-Type", "application/json") + .header("x-topgg-signature", Mocks.signature(SECRET, payload)) + .header("x-topgg-trace", TRACE) + .post(Entity.entity(payload, MediaType.APPLICATION_JSON)); + + Assertions.assertEquals(200, response.getStatus()); + Assertions.assertEquals("dw:" + name + "," + TRACE, response.readEntity(String.class)); + } + + @Test + public void integrationCreate() throws NoSuchAlgorithmException, InvalidKeyException { + send("integrationCreate", MOCKS.integrationCreatePayload); + } + + @Test + public void integrationDelete() throws NoSuchAlgorithmException, InvalidKeyException { + send("integrationDelete", MOCKS.integrationDeletePayload); + } + + @Test + public void test() throws NoSuchAlgorithmException, InvalidKeyException { + send("test", MOCKS.testPayload); + } + + @Test + public void voteCreate() throws NoSuchAlgorithmException, InvalidKeyException { + send("voteCreate", MOCKS.voteCreatePayload); + } +} diff --git a/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java new file mode 100644 index 0000000..3bcfdcf --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/eclipsejetty/CustomWebhooks.java @@ -0,0 +1,51 @@ +package org.discordbots.webhooks.eclipsejetty; + +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.discordbots.webhooks.payload.IntegrationCreatePayload; +import org.discordbots.webhooks.payload.IntegrationDeletePayload; +import org.discordbots.webhooks.payload.TestPayload; +import org.discordbots.webhooks.payload.VoteCreatePayload; + +public class CustomWebhooks extends DBLWebhooks { + public CustomWebhooks() { + super(System.getenv("TOPGG_WEBHOOK_SECRET")); + } + + private void reply(final String name, final HttpServletResponse response, final String trace) { + try { + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().write("ej:" + name + "," + trace); + } catch (final IOException ignored) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + @Override + public void onIntegrationCreate( + final HttpServletResponse response, + final IntegrationCreatePayload payload, + final String trace) { + reply("integrationCreate", response, trace); + } + + @Override + public void onIntegrationDelete( + final HttpServletResponse response, + final IntegrationDeletePayload payload, + final String trace) { + reply("integrationDelete", response, trace); + } + + @Override + public void onTest( + final HttpServletResponse response, final TestPayload payload, final String trace) { + reply("test", response, trace); + } + + @Override + public void onVoteCreate( + final HttpServletResponse response, final VoteCreatePayload payload, final String trace) { + reply("voteCreate", response, trace); + } +} diff --git a/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java b/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java new file mode 100644 index 0000000..7450a20 --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/eclipsejetty/DBLEclipseJettyWebhooksTest.java @@ -0,0 +1,105 @@ +package org.discordbots.webhooks.eclipsejetty; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.URI; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import org.discordbots.webhooks.Mocks; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.server.Server; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class DBLEclipseJettyWebhooksTest { + private static Server SERVER = null; + + private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET"); + private static final String TRACE = "trace"; + private static Mocks MOCKS; + + @BeforeAll + public static void setup() throws IOException, NullPointerException, Exception { + MOCKS = new Mocks(); + + SERVER = new Server(8080); + + final ServletContextHandler context = new ServletContextHandler(); + + context.setContextPath("/"); + context.addServlet(new ServletHolder(new CustomWebhooks()), "/webhook"); + + SERVER.setHandler(context); + SERVER.start(); + } + + private void send(final String name, final String payload) + throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException { + final HttpURLConnection connection = + (HttpURLConnection) URI.create("http://localhost:8080/webhook").toURL().openConnection(); + + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("x-topgg-signature", Mocks.signature(SECRET, payload)); + connection.setRequestProperty("x-topgg-trace", TRACE); + connection.setDoOutput(true); + + try (final OutputStream outputStream = connection.getOutputStream()) { + final byte[] payloadBytes = payload.getBytes("utf-8"); + + outputStream.write(payloadBytes, 0, payloadBytes.length); + } + + Assertions.assertEquals(200, connection.getResponseCode()); + + try (final BufferedReader reader = + new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8"))) { + final StringBuilder response = new StringBuilder(); + String responseLine; + + while ((responseLine = reader.readLine()) != null) { + response.append(responseLine.trim()); + } + + Assertions.assertEquals("ej:" + name + "," + TRACE, response.toString()); + } + } + + @Test + public void integrationCreate() + throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException { + send("integrationCreate", MOCKS.integrationCreatePayload); + } + + @Test + public void integrationDelete() + throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException { + send("integrationDelete", MOCKS.integrationDeletePayload); + } + + @Test + public void test() + throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException { + send("test", MOCKS.testPayload); + } + + @Test + public void voteCreate() + throws NoSuchAlgorithmException, InvalidKeyException, ProtocolException, IOException { + send("voteCreate", MOCKS.voteCreatePayload); + } + + @AfterAll + public static void cleanup() throws Exception { + if (SERVER != null) { + SERVER.stop(); + } + } +} diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java b/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java new file mode 100644 index 0000000..e5c584f --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/springboot/CustomServer.java @@ -0,0 +1,11 @@ +package org.discordbots.webhooks.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CustomServer { + public static void main(final String[] args) throws Exception { + SpringApplication.run(CustomServer.class, args); + } +} diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java b/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java new file mode 100644 index 0000000..9805872 --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/springboot/CustomServerSecurityConfiguration.java @@ -0,0 +1,16 @@ +package org.discordbots.webhooks.springboot; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class CustomServerSecurityConfiguration { + @Bean + public SecurityFilterChain filterChain(final HttpSecurity http) { + return http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .build(); + } +} diff --git a/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java b/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java new file mode 100644 index 0000000..c6eca75 --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/springboot/CustomWebhooks.java @@ -0,0 +1,49 @@ +package org.discordbots.webhooks.springboot; + +import org.discordbots.webhooks.payload.IntegrationCreatePayload; +import org.discordbots.webhooks.payload.IntegrationDeletePayload; +import org.discordbots.webhooks.payload.TestPayload; +import org.discordbots.webhooks.payload.VoteCreatePayload; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class CustomWebhooks extends DBLWebhooks { + public CustomWebhooks() { + super(System.getenv("TOPGG_WEBHOOK_SECRET")); + } + + @PostMapping("/webhook") + public ResponseEntity main( + @RequestBody final String body, + @RequestHeader("x-topgg-signature") final String signature, + @RequestHeader("x-topgg-trace") final String trace) { + return dispatch(body, signature, trace); + } + + @Override + public ResponseEntity onIntegrationCreate( + final IntegrationCreatePayload payload, final String trace) { + return ResponseEntity.status(HttpStatus.OK).body("sb:integrationCreate," + trace); + } + + @Override + public ResponseEntity onIntegrationDelete( + final IntegrationDeletePayload payload, final String trace) { + return ResponseEntity.status(HttpStatus.OK).body("sb:integrationDelete," + trace); + } + + @Override + public ResponseEntity onTest(final TestPayload payload, final String trace) { + return ResponseEntity.status(HttpStatus.OK).body("sb:test," + trace); + } + + @Override + public ResponseEntity onVoteCreate(final VoteCreatePayload payload, final String trace) { + return ResponseEntity.status(HttpStatus.OK).body("sb:voteCreate," + trace); + } +} diff --git a/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java b/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java new file mode 100644 index 0000000..8405f4e --- /dev/null +++ b/src/test/java/org/discordbots/webhooks/springboot/DBLSpringBootWebhooksTest.java @@ -0,0 +1,59 @@ +package org.discordbots.webhooks.springboot; + +import java.io.IOException; +import org.discordbots.webhooks.Mocks; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest +@AutoConfigureMockMvc +public class DBLSpringBootWebhooksTest { + private static final String SECRET = System.getenv("TOPGG_WEBHOOK_SECRET"); + private static final String TRACE = "trace"; + private static Mocks MOCKS; + + @Autowired private MockMvc mvc; + + @BeforeAll + public static void setup() throws IOException, NullPointerException { + MOCKS = new Mocks(); + } + + private void send(final String name, final String payload) throws IOException, Exception { + mvc.perform( + MockMvcRequestBuilders.post("/webhook") + .content(payload) + .header("x-topgg-signature", Mocks.signature(SECRET, payload)) + .header("x-topgg-trace", TRACE) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is(200)) + .andExpect(MockMvcResultMatchers.content().string("sb:" + name + "," + TRACE)); + } + + @Test + public void integrationCreate() throws IOException, Exception { + send("integrationCreate", MOCKS.integrationCreatePayload); + } + + @Test + public void integrationDelete() throws IOException, Exception { + send("integrationDelete", MOCKS.integrationDeletePayload); + } + + @Test + public void test() throws IOException, Exception { + send("test", MOCKS.testPayload); + } + + @Test + public void voteCreate() throws IOException, Exception { + send("voteCreate", MOCKS.voteCreatePayload); + } +} diff --git a/src/test/resources/GetSelfResponse.json b/src/test/resources/GetSelfResponse.json new file mode 100644 index 0000000..e74997b --- /dev/null +++ b/src/test/resources/GetSelfResponse.json @@ -0,0 +1,16 @@ +{ + "id": "364806029876555776", + "name": "Top.gg Lib Dev API Access", + "type": "bot", + "platform": "discord", + "headline": "API access for Top.gg Library Developers", + "tags": [ + "api", + "library", + "topgg" + ], + "votes": 4, + "votes_total": 34, + "review_score": 5, + "review_count": 2 +} \ No newline at end of file diff --git a/src/test/resources/GetVoteResponse.json b/src/test/resources/GetVoteResponse.json new file mode 100644 index 0000000..c6a0227 --- /dev/null +++ b/src/test/resources/GetVoteResponse.json @@ -0,0 +1,5 @@ +{ + "created_at": "2026-02-25T22:35:36.978392+00:00", + "expires_at": "2026-02-26T10:35:36.978392+00:00", + "weight": 1 +} \ No newline at end of file diff --git a/src/test/resources/GetVotesResponse.json b/src/test/resources/GetVotesResponse.json new file mode 100644 index 0000000..5eab378 --- /dev/null +++ b/src/test/resources/GetVotesResponse.json @@ -0,0 +1,33 @@ +{ + "cursor": "", + "data": [ + { + "user_id": "800506814562787328", + "platform_id": "1461830808796139662", + "weight": 2, + "created_at": "2026-01-17T23:36:06.34732Z", + "expires_at": "2026-01-18T11:36:06.34732Z" + }, + { + "user_id": "316026718115037184", + "platform_id": "481068576363773972", + "weight": 2, + "created_at": "2026-02-20T05:43:58.392411Z", + "expires_at": "2026-02-20T17:43:58.392411Z" + }, + { + "user_id": "794153497215045632", + "platform_id": "1425259851600101457", + "weight": 2, + "created_at": "2026-02-21T18:59:20.660734Z", + "expires_at": "2026-02-22T06:59:20.660734Z" + }, + { + "user_id": "8226924471638491136", + "platform_id": "661200758510977084", + "weight": 1, + "created_at": "2026-02-25T22:35:36.978392Z", + "expires_at": "2026-02-26T10:35:36.978392Z" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/IntegrationCreatePayload.json b/src/test/resources/IntegrationCreatePayload.json new file mode 100644 index 0000000..77df6b0 --- /dev/null +++ b/src/test/resources/IntegrationCreatePayload.json @@ -0,0 +1,19 @@ +{ + "type": "integration.create", + "data": { + "connection_id": "112402021105124", + "webhook_secret": "whs_abcd", + "project": { + "id": "1230954036934033243", + "platform": "discord", + "platform_id": "3949456393249234923", + "type": "bot" + }, + "user": { + "id": "3949456393249234923", + "platform_id": "3949456393249234923", + "name": "username", + "avatar_url": "" + } + } +} \ No newline at end of file diff --git a/src/test/resources/IntegrationDeletePayload.json b/src/test/resources/IntegrationDeletePayload.json new file mode 100644 index 0000000..cb44375 --- /dev/null +++ b/src/test/resources/IntegrationDeletePayload.json @@ -0,0 +1,6 @@ +{ + "type": "integration.delete", + "data": { + "connection_id": "112402021105124" + } +} \ No newline at end of file diff --git a/src/test/resources/LeadResponse.json b/src/test/resources/LeadResponse.json new file mode 100644 index 0000000..52b54a3 --- /dev/null +++ b/src/test/resources/LeadResponse.json @@ -0,0 +1,3 @@ +{ + "error": "Not Found" +} \ No newline at end of file diff --git a/src/test/resources/PostCommands.json b/src/test/resources/PostCommands.json new file mode 100644 index 0000000..071db3e --- /dev/null +++ b/src/test/resources/PostCommands.json @@ -0,0 +1,11 @@ +[ + { + "id": "1", + "type": 1, + "application_id": "1", + "name": "test", + "description": "command description", + "default_member_permissions": "", + "version": "1" + } +] \ No newline at end of file diff --git a/src/test/resources/TestPayload.json b/src/test/resources/TestPayload.json new file mode 100644 index 0000000..b7a7432 --- /dev/null +++ b/src/test/resources/TestPayload.json @@ -0,0 +1,17 @@ +{ + "type": "webhook.test", + "data": { + "user": { + "id": "160105994217586689", + "platform_id": "160105994217586689", + "name": "username", + "avatar_url": "" + }, + "project": { + "id": "803190510032756736", + "type": "bot", + "platform": "discord", + "platform_id": "160105994217586689" + } + } +} \ No newline at end of file diff --git a/src/test/resources/VoteCreatePayload.json b/src/test/resources/VoteCreatePayload.json new file mode 100644 index 0000000..6850196 --- /dev/null +++ b/src/test/resources/VoteCreatePayload.json @@ -0,0 +1,21 @@ +{ + "type": "vote.create", + "data": { + "id": "808499215864008704", + "weight": 1, + "created_at": "2026-02-09T00:47:14.2510149+00:00", + "expires_at": "2026-02-09T12:47:14.2510149+00:00", + "project": { + "id": "803190510032756736", + "type": "bot", + "platform": "discord", + "platform_id": "160105994217586689" + }, + "user": { + "id": "160105994217586689", + "platform_id": "160105994217586689", + "name": "username", + "avatar_url": "" + } + } +} \ No newline at end of file diff --git a/src/test/resources/dropwizard-test-config.yml b/src/test/resources/dropwizard-test-config.yml new file mode 100644 index 0000000..c8aa4d6 --- /dev/null +++ b/src/test/resources/dropwizard-test-config.yml @@ -0,0 +1,7 @@ +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 \ No newline at end of file