diff --git a/inngest-spring-boot-demo/build.gradle.kts b/inngest-spring-boot-demo/build.gradle.kts index ec94a5be..06c0dc62 100644 --- a/inngest-spring-boot-demo/build.gradle.kts +++ b/inngest-spring-boot-demo/build.gradle.kts @@ -26,6 +26,12 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") testImplementation("org.springframework.boot:spring-boot-starter-test") + + if (JavaVersion.current().isJava11Compatible) { + testImplementation("uk.org.webcompere:system-stubs-jupiter:2.1.6") + } else { + testImplementation("uk.org.webcompere:system-stubs-jupiter:1.2.1") + } } dependencyManagement { @@ -39,6 +45,9 @@ dependencyManagement { tasks.withType { useJUnitPlatform() systemProperty("junit.jupiter.execution.parallel.enabled", true) + systemProperty("test-group", "unit-test") + + jvmArgs = listOf("-Dnet.bytebuddy.experimental=true") testLogging { events = setOf( diff --git a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/CloudModeIntrospectionTest.java b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/CloudModeIntrospectionTest.java new file mode 100644 index 00000000..9363a031 --- /dev/null +++ b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/CloudModeIntrospectionTest.java @@ -0,0 +1,139 @@ +package com.inngest.springbootdemo; + +import com.inngest.*; +import com.inngest.signingkey.BearerTokenKt; +import com.inngest.signingkey.SignatureVerificationKt; +import com.inngest.springboot.InngestConfiguration; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; +import uk.org.webcompere.systemstubs.environment.EnvironmentVariables; +import uk.org.webcompere.systemstubs.jupiter.SystemStub; +import uk.org.webcompere.systemstubs.jupiter.SystemStubsExtension; + +import java.util.HashMap; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class ProductionConfiguration extends InngestConfiguration { + + public static final String INNGEST_APP_ID = "spring_test_prod_demo"; + + @Override + protected HashMap functions() { + return new HashMap<>(); + } + + @Override + protected Inngest inngestClient() { + return new Inngest(INNGEST_APP_ID); + } + + @Override + protected ServeConfig serve(Inngest client) { + return new ServeConfig(client); + } + + @Bean + protected CommHandler commHandler(@Autowired Inngest inngestClient) { + ServeConfig serveConfig = new ServeConfig(inngestClient); + return new CommHandler(functions(), inngestClient, serveConfig, SupportedFrameworkName.SpringBoot); + } +} + +@ExtendWith(SystemStubsExtension.class) +public class CloudModeIntrospectionTest { + + private static final String productionSigningKey = "signkey-prod-2a89e554826a40672684e75eee6e34909b45aa4fd04fff5ff49bbe28c24ef424"; + private static final String productionEventKey = "test"; + @SystemStub + private static EnvironmentVariables environmentVariables; + + @BeforeAll + static void beforeAll() { + environmentVariables.set("INNGEST_DEV", "0"); + environmentVariables.set("INNGEST_SIGNING_KEY", productionSigningKey); + environmentVariables.set("INNGEST_EVENT_KEY", productionEventKey); + } + + // The nested class is useful for setting the environment variables before the configuration class (Beans) runs. + // https://www.baeldung.com/java-system-stubs#environment-and-property-overrides-for-junit-5-spring-tests + @Import(ProductionConfiguration.class) + @WebMvcTest(DemoController.class) + @Nested + @EnabledIfSystemProperty(named = "test-group", matches = "unit-test") + class InnerSpringTest { + @Autowired + private MockMvc mockMvc; + + @Test + public void shouldReturnInsecureIntrospectionWhenSignatureIsMissing() throws Exception { + mockMvc.perform(get("/api/inngest").header("Host", "localhost:8080")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(header().string(InngestHeaderKey.Framework.getValue(), "springboot")) + .andExpect(jsonPath("$.authentication_succeeded").value(false)) + .andExpect(jsonPath("$.function_count").isNumber()) + .andExpect(jsonPath("$.has_event_key").value(true)) + .andExpect(jsonPath("$.has_signing_key").value(true)) + .andExpect(jsonPath("$.mode").value("cloud")) + .andExpect(jsonPath("$.schema_version").value("2024-05-24")); + } + + @Test + public void shouldReturnInsecureIntrospectionWhenSignatureIsInvalid() throws Exception { + mockMvc.perform(get("/api/inngest") + .header("Host", "localhost:8080") + .header(InngestHeaderKey.Signature.getValue(), "invalid-signature")) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(header().string(InngestHeaderKey.Framework.getValue(), "springboot")) + .andExpect(jsonPath("$.authentication_succeeded").value(false)) + .andExpect(jsonPath("$.function_count").isNumber()) + .andExpect(jsonPath("$.has_event_key").value(true)) + .andExpect(jsonPath("$.has_signing_key").value(true)) + .andExpect(jsonPath("$.mode").value("cloud")) + .andExpect(jsonPath("$.schema_version").value("2024-05-24")); + } + + @Test + public void shouldReturnSecureIntrospectionWhenSignatureIsValid() throws Exception { + long currentTimestamp = System.currentTimeMillis() / 1000; + + String signature = SignatureVerificationKt.signRequest("", currentTimestamp, productionSigningKey); + String formattedSignature = String.format("s=%s&t=%d", signature, currentTimestamp); + + String expectedSigningKeyHash = BearerTokenKt.hashedSigningKey(productionSigningKey); + + mockMvc.perform(get("/api/inngest") + .header("Host", "localhost:8080") + .header(InngestHeaderKey.Signature.getValue(), formattedSignature)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(header().string(InngestHeaderKey.Framework.getValue(), "springboot")) + .andExpect(jsonPath("$.authentication_succeeded").value(true)) + .andExpect(jsonPath("$.function_count").isNumber()) + .andExpect(jsonPath("$.has_event_key").value(true)) + .andExpect(jsonPath("$.has_signing_key").value(true)) + .andExpect(jsonPath("$.mode").value("cloud")) + .andExpect(jsonPath("$.schema_version").value("2024-05-24")) + .andExpect(jsonPath("$.api_origin").value("https://api.inngest.com/")) + .andExpect(jsonPath("$.app_id").value(ProductionConfiguration.INNGEST_APP_ID)) + .andExpect(jsonPath("$.env").value("prod")) + .andExpect(jsonPath("$.event_api_origin").value("https://inn.gs/")) + .andExpect(jsonPath("$.framework").value("springboot")) + .andExpect(jsonPath("$.sdk_language").value("java")) + .andExpect(jsonPath("$.event_key_hash").value("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08")) + .andExpect(jsonPath("$.sdk_version").value(Version.Companion.getVersion())) + .andExpect(jsonPath("$.signing_key_hash").value(expectedSigningKeyHash)); + } + } +} diff --git a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/IntrospectTest.java b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DevModeIntrospectionTest.java similarity index 88% rename from inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/IntrospectTest.java rename to inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DevModeIntrospectionTest.java index 097d3a40..7b59cfee 100644 --- a/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/IntrospectTest.java +++ b/inngest-spring-boot-demo/src/test/java/com/inngest/springbootdemo/DevModeIntrospectionTest.java @@ -2,6 +2,7 @@ import com.inngest.InngestHeaderKey; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; @@ -12,12 +13,13 @@ @Import(DemoTestConfiguration.class) @WebMvcTest(DemoController.class) -public class IntrospectTest { +public class DevModeIntrospectionTest { @Autowired private MockMvc mockMvc; @Test + @EnabledIfSystemProperty(named = "test-group", matches = "unit-test") public void shouldReturnInsecureIntrospectPayload() throws Exception { mockMvc.perform(get("/api/inngest").header("Host", "localhost:8080")) .andExpect(status().isOk())