diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml new file mode 100644 index 000000000..e3f428b4f --- /dev/null +++ b/.github/workflows/e2e-tests.yaml @@ -0,0 +1,66 @@ +## +# Copyright (C) 2024 Hedera Hashgraph, LLC +# +# 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. +## +name: "E2E Test Suites" +on: + push: + branches: + - main + - release/* + pull_request: + branches: + - "*" + +defaults: + run: + shell: bash + +env: + GRADLE_EXEC: ./gradlew + +jobs: + e2e-tests: + runs-on: block-node-linux-medium + steps: + - name: Harden Runner + uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 # v2.8.1 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + with: + fetch-depth: 0 + + - name: Expand Shallow Clone for Spotless + run: | + if [ -f .git/shallow ]; then + git fetch --unshallow --no-recurse-submodules + else + echo "Repository is not shallow, no need to unshallow." + fi + + - name: Set up JDK 21 + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + with: + distribution: 'temurin' + java-version: '21' + + - name: Build application + run: ${{ env.GRADLE_EXEC }} build + + - name: Run Acceptance Tests + id: acceptance-tests + run: ${GRADLE_EXEC} runSuites diff --git a/.github/workflows/smoke-test.yaml b/.github/workflows/smoke-test.yaml index e2935a070..a8c1f0de4 100644 --- a/.github/workflows/smoke-test.yaml +++ b/.github/workflows/smoke-test.yaml @@ -81,7 +81,7 @@ jobs: - name: Run application in background, capture logs in app.log run: | - ${{ env.GRADLE_EXEC }} run 2> server/src/test/resources/app.log < /dev/null & + ${{ env.GRADLE_EXEC }} run -x :suites:run 2> server/src/test/resources/app.log < /dev/null & echo "Application started with PID $APP_PID" sleep 10 diff --git a/buildSrc/src/main/kotlin/com.hedera.block.jpms-modules.gradle.kts b/buildSrc/src/main/kotlin/com.hedera.block.jpms-modules.gradle.kts index 964d27a85..600342b75 100644 --- a/buildSrc/src/main/kotlin/com.hedera.block.jpms-modules.gradle.kts +++ b/buildSrc/src/main/kotlin/com.hedera.block.jpms-modules.gradle.kts @@ -167,6 +167,14 @@ extraJavaModuleInfo { module("org.jetbrains.kotlinx:kotlinx-metadata-jvm", "kotlinx.metadata.jvm") // Test clients only + module("com.github.docker-java:docker-java-api", "com.github.dockerjava.api") + module("com.github.docker-java:docker-java-transport", "com.github.dockerjava.transport") + module( + "com.github.docker-java:docker-java-transport-zerodep", + "com.github.dockerjava.transport.zerodep" + ) + module("org.slf4j:slf4j-api", "org.slf4j") { patchRealModule() } + module("io.github.cdimascio:java-dotenv", "io.github.cdimascio") module("com.google.protobuf:protobuf-java-util", "com.google.protobuf.util") module("com.squareup:javapoet", "com.squareup.javapoet") { exportAllPackages() diff --git a/buildSrc/src/main/kotlin/com.hedera.block.suites.gradle.kts b/buildSrc/src/main/kotlin/com.hedera.block.suites.gradle.kts new file mode 100644 index 000000000..f5361b330 --- /dev/null +++ b/buildSrc/src/main/kotlin/com.hedera.block.suites.gradle.kts @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * 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. + */ + +plugins { + id("application") + id("com.hedera.block.conventions") + id("me.champeau.jmh") +} + +val maven = publishing.publications.create("maven") { from(components["java"]) } + +signing.sign(maven) diff --git a/gradle/modules.properties b/gradle/modules.properties index 8290bd368..fcc1dcb94 100644 --- a/gradle/modules.properties +++ b/gradle/modules.properties @@ -31,6 +31,13 @@ org.apache.commons.io=commons-io:commons-io org.apache.commons.lang3=org.apache.commons:commons-lang3 org.apache.commons.compress=org.apache.commons:commons-compress +org.testcontainers=org.testcontainers:testcontainers +org.testcontainers.junit-jupiter=org.testcontainers:junit-jupiter +com.github.dockerjava.api=com.github.docker-java:docker-java-api +com.github.docker-java.transport.zerodep=com.github.docker-java:docker-java-transport-zerodep +com.github.docker-java.transport.httpclient5=com.github.docker-java:docker-java-transport-httpclient5 +io.github.cdimascio=io.github.cdimascio:java-dotenv + java.annotation=javax.annotation:javax.annotation-api org.apache.logging.log4j.slf4j2.impl=org.apache.logging.log4j:log4j-slf4j2-impl diff --git a/server/docker/Dockerfile b/server/docker/Dockerfile index 50b1c5ad4..0af9bdb80 100644 --- a/server/docker/Dockerfile +++ b/server/docker/Dockerfile @@ -27,5 +27,10 @@ RUN tar -xvf server-${VERSION}.tar # Copy the logging properties file COPY logging.properties logging.properties +# HEALTHCHECK for liveness and readiness +HEALTHCHECK --interval=30s --timeout=10s --start-period=3s --retries=3 \ + CMD curl -f http://localhost:8080/healthz/livez || exit 1 && \ + curl -f http://localhost:8080/healthz/readyz || exit 1 + # RUN the bin script for starting the server ENTRYPOINT ["/bin/bash", "-c", "/app/server-${VERSION}/bin/server"] diff --git a/settings.gradle.kts b/settings.gradle.kts index 5cdb9b67e..0d1d79c6d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -19,6 +19,7 @@ plugins { } // Include the subprojects +include(":suites") include(":stream") include(":server") include(":simulator") @@ -95,9 +96,13 @@ dependencyResolutionManagement { // Testing only versions version("org.assertj.core", "3.23.1") version("org.junit.jupiter.api", "5.10.2") + version("org.junit.platform", "1.11.0") version("org.mockito", "5.8.0") version("org.mockito.junit.jupiter", "5.8.0") - + version("org.testcontainers", "1.20.1") + version("org.testcontainers.junit-jupiter", "1.20.1") + version("com.github.docker-java", "3.4.0") + version("io.github.cdimascio", "5.2.2") } } } diff --git a/suites/build.gradle.kts b/suites/build.gradle.kts new file mode 100644 index 000000000..4ed80e300 --- /dev/null +++ b/suites/build.gradle.kts @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * 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. + */ + +plugins { + id("application") + id("com.hedera.block.suites") +} + +description = "Hedera Block Node E2E Suites" + +application { + mainModule = "com.hedera.block.suites" + mainClass = "com.hedera.block.suites.BaseSuite" +} + +mainModuleInfo { + requires("org.junit.jupiter.api") + requires("org.junit.platform.suite.api") + requires("org.testcontainers") + requires("io.github.cdimascio") + runtimeOnly("org.testcontainers.junit-jupiter") + runtimeOnly("org.junit.jupiter.engine") +} + +val updateDockerEnv = + tasks.register("updateDockerEnv") { + description = + "Creates the .env file in the docker folder that contains environment variables for Docker" + group = "docker" + + workingDir(layout.projectDirectory.dir("../server/docker")) + commandLine("./update-env.sh", project.version) + } + +// Task to build the Docker image +tasks.register("createDockerImage") { + description = "Creates the Docker image of the Block Node Server based on the current version" + group = "docker" + + dependsOn(updateDockerEnv, tasks.assemble) + workingDir(layout.projectDirectory.dir("../server/docker")) + commandLine("./docker-build.sh", project.version, layout.projectDirectory.dir("..").asFile) +} + +tasks.register("runSuites") { + description = "Runs E2E Test Suites" + group = "suites" + dependsOn("createDockerImage") + + useJUnitPlatform() + testLogging { events("passed", "skipped", "failed") } + testClassesDirs = sourceSets["main"].output.classesDirs + classpath = sourceSets["main"].runtimeClasspath +} diff --git a/suites/src/main/java/com/hedera/block/suites/BaseSuite.java b/suites/src/main/java/com/hedera/block/suites/BaseSuite.java new file mode 100644 index 000000000..9e976be6e --- /dev/null +++ b/suites/src/main/java/com/hedera/block/suites/BaseSuite.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * 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.hedera.block.suites; + +import io.github.cdimascio.dotenv.Dotenv; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * BaseSuite is an abstract class that provides common setup and teardown functionality for test + * suites using Testcontainers to manage a Docker container for the Block Node server. + * + *

This class is responsible for: + * + *

    + *
  • Starting a Docker container running the Block Node Application with a specified version. + *
  • Stopping the container after tests have been executed. + *
+ * + *

The Block Node Application version is retrieved dynamically from an environment file (.env). + */ +public abstract class BaseSuite { + + /** Container running the Block Node Application */ + protected static GenericContainer blockNodeContainer; + + /** Port that is used by the Block Node Application */ + protected static int blockNodePort; + + /** + * Default constructor for the BaseSuite class. + * + *

This constructor can be used by subclasses or the testing framework to initialize the + * BaseSuite. It does not perform any additional setup. + */ + public BaseSuite() { + // No additional setup required + } + + /** + * Setup method to be executed before all tests. + * + *

This method initializes the Block Node server container using Testcontainers. + */ + @BeforeAll + public static void setup() { + blockNodeContainer = getConfiguration(); + blockNodeContainer.start(); + } + + /** + * Teardown method to be executed after all tests. + * + *

This method stops the Block Node server container if it is running. It ensures that + * resources are cleaned up after the test suite execution is complete. + */ + @AfterAll + public static void teardown() { + if (blockNodeContainer != null) { + blockNodeContainer.stop(); + } + } + + /** + * Retrieves the configuration for the Block Node server container. + * + *

This method initializes the Block Node container with the version retrieved from the .env + * file. It configures the container and returns it. + * + *

Specific configuration steps include: + * + *

    + *
  • Setting the environment variable "VERSION" from the .env file. + *
  • Exposing the default gRPC port (8080). + *
  • Using the Testcontainers health check mechanism to ensure the container is ready. + *
+ * + * @return a configured {@link GenericContainer} instance for the Block Node server + */ + public static GenericContainer getConfiguration() { + String blockNodeVersion = BaseSuite.getBlockNodeVersion(); + blockNodePort = 8080; + blockNodeContainer = + new GenericContainer<>( + DockerImageName.parse("block-node-server:" + blockNodeVersion)) + .withExposedPorts(blockNodePort) + .withEnv("VERSION", blockNodeVersion) + .waitingFor(Wait.forListeningPort()) + .waitingFor(Wait.forHealthcheck()); + return blockNodeContainer; + } + + /** + * Retrieves the Block Node server version from the .env file. + * + *

This method loads the .env file from the "../server/docker" directory and extracts the + * value of the "VERSION" environment variable, which represents the version of the Block Node + * server to be used in the container. + * + * @return the version of the Block Node server as a string + */ + private static String getBlockNodeVersion() { + Dotenv dotenv = Dotenv.configure().directory("../server/docker").filename(".env").load(); + + return dotenv.get("VERSION"); + } +} diff --git a/suites/src/main/java/com/hedera/block/suites/grpc/GrpcTestSuites.java b/suites/src/main/java/com/hedera/block/suites/grpc/GrpcTestSuites.java new file mode 100644 index 000000000..9210c8f9b --- /dev/null +++ b/suites/src/main/java/com/hedera/block/suites/grpc/GrpcTestSuites.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * 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.hedera.block.suites.grpc; + +import com.hedera.block.suites.grpc.negative.NegativeServerAvailabilityTests; +import com.hedera.block.suites.grpc.positive.PositiveServerAvailabilityTests; +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +/** + * Test suite for running gRPC server availability tests, including both positive and negative test + * scenarios. + * + *

This suite aggregates the tests from {@link PositiveServerAvailabilityTests} and {@link + * NegativeServerAvailabilityTests}. The {@code @Suite} annotation allows running all selected + * classes in a single test run. + */ +@Suite +@SelectClasses({PositiveServerAvailabilityTests.class, NegativeServerAvailabilityTests.class}) +public class GrpcTestSuites { + + /** + * Default constructor for the {@link GrpcTestSuites} class. This constructor is empty as it + * does not need to perform any initialization. + */ + public GrpcTestSuites() {} +} diff --git a/suites/src/main/java/com/hedera/block/suites/grpc/negative/NegativeServerAvailabilityTests.java b/suites/src/main/java/com/hedera/block/suites/grpc/negative/NegativeServerAvailabilityTests.java new file mode 100644 index 000000000..86e953eec --- /dev/null +++ b/suites/src/main/java/com/hedera/block/suites/grpc/negative/NegativeServerAvailabilityTests.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * 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.hedera.block.suites.grpc.negative; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.block.suites.BaseSuite; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.ContainerLaunchException; + +/** + * Test class for verifying negative scenarios related to server availability for the Block Node + * application. This class is part of the gRPC module and aims to test how the Block Node handles + * incorrect configurations or failures during server startup. + * + *

Inherits from {@link BaseSuite} to reuse the container setup and teardown logic for the Block + * Node. + */ +@DisplayName("Negative Server Availability Tests") +public class NegativeServerAvailabilityTests extends BaseSuite { + + /** + * Default constructor for the {@link NegativeServerAvailabilityTests} class. This constructor + * does not require any specific initialization. + */ + public NegativeServerAvailabilityTests() {} + + /** + * Clean up method executed after each test. + * + *

This method stops the running container, resets the container configuration by retrieving + * a new one through {@link BaseSuite#getConfiguration()}, and then starts the Block Node + * container again. + */ + @AfterEach + public void cleanUp() { + blockNodeContainer.stop(); + blockNodeContainer = getConfiguration(); + blockNodeContainer.start(); + } + + /** + * Test to verify that the Block Node server fails to start when provided with an invalid + * configuration. + * + *

Specifically, this test modifies the environment variable "VERSION" with an invalid value, + * which causes the server to fail during startup. The test expects a {@link + * ContainerLaunchException} to be thrown. + */ + @Test + public void serverStartupThrowsForInvalidConfiguration() { + blockNodeContainer.stop(); + blockNodeContainer.addEnv("VERSION", "Wrong!"); + assertThrows( + ContainerLaunchException.class, + () -> blockNodeContainer.start(), + "Starting the Block Node container with invalid configuration should throw" + + " ContainerLaunchException."); + } +} diff --git a/suites/src/main/java/com/hedera/block/suites/grpc/positive/PositiveServerAvailabilityTests.java b/suites/src/main/java/com/hedera/block/suites/grpc/positive/PositiveServerAvailabilityTests.java new file mode 100644 index 000000000..e675e3f56 --- /dev/null +++ b/suites/src/main/java/com/hedera/block/suites/grpc/positive/PositiveServerAvailabilityTests.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2022-2024 Hedera Hashgraph, LLC + * + * 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.hedera.block.suites.grpc.positive; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hedera.block.suites.BaseSuite; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Test class for verifying the positive scenarios for server availability, specifically related to + * the gRPC server. This class contains tests to check that the gRPC server starts successfully and + * listens on the correct port. + * + *

Inherits from {@link BaseSuite} to reuse the container setup and teardown logic for the Block + * Node. + */ +@DisplayName("Positive Server Availability Tests") +public class PositiveServerAvailabilityTests extends BaseSuite { + + /** Default constructor for the {@link PositiveServerAvailabilityTests} class. */ + public PositiveServerAvailabilityTests() {} + + /** + * Test to verify that the gRPC server starts successfully. + * + *

The test checks if the Block Node container is running and marked as healthy. + */ + @Test + public void verifyGrpcServerStartsSuccessfully() { + assertTrue(blockNodeContainer.isRunning(), "Block Node container should be running."); + assertTrue(blockNodeContainer.isHealthy(), "Block Node container should be healthy."); + } + + /** + * Test to verify that the gRPC server is listening on the correct port. + * + *

The test asserts that the container is running, exposes exactly one port, and that the + * exposed port matches the expected gRPC server port. + */ + @Test + public void verifyGrpcServerListeningOnCorrectPort() { + assertTrue(blockNodeContainer.isRunning(), "Block Node container should be running."); + assertEquals( + 1, + blockNodeContainer.getExposedPorts().size(), + "There should be exactly one exposed port."); + assertEquals( + blockNodePort, + blockNodeContainer.getExposedPorts().getFirst(), + "The exposed port should match the expected gRPC server port."); + } +}