diff --git a/.gitea/workflows/integration_test.yaml b/.gitea/workflows/integration_test.yaml new file mode 100644 index 0000000..4c4ee37 --- /dev/null +++ b/.gitea/workflows/integration_test.yaml @@ -0,0 +1,26 @@ +name: Daily cloudflare API integration test +on: + push: + branches: [ main ] + schedule: + - cron: '0 */12 * * *' # Every hour + workflow_dispatch: + +jobs: + cloudflare-api-test: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: JDK setup + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - name: Run integration tests with Cloudflare API + env: + SPRING_PROFILES_ACTIVE: integration + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + CLOUDFLARE_API_KEY: ${{ secrets.CLOUDFLARE_API_KEY }} + CLOUDFLARE_EMAIL: hitanshu98@gmail.com + run: ./gradlew integrationTestOnly \ No newline at end of file diff --git a/.gitea/workflows/test_image_build_push.yml b/.gitea/workflows/test_image_build_push.yml index f53e140..a4ed4ea 100644 --- a/.gitea/workflows/test_image_build_push.yml +++ b/.gitea/workflows/test_image_build_push.yml @@ -1,5 +1,5 @@ name: sample gradle build and test -run-name: Build started by $ {{gitea.actor}} +run-name: Build started by ${{ gitea.actor }} on: push: branches: [test] @@ -13,17 +13,19 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Get new version id: new_version run: | VERSION=$(git describe --tags --abbrev=0) - echo ${VERSION} + echo "Current version: ${VERSION}" MAJOR=$(echo ${VERSION} | cut -d "." -f 1) MINOR=$(echo ${VERSION} | cut -d "." -f 2) PATCH=$(echo ${VERSION} | cut -d "." -f 3) - NEW_PATCH=$(( ${PATCH} + 1)) - echo ${NEW_PATCH} - echo "new_version=$(echo "${MAJOR}.${MINOR}.${NEW_PATCH}")" >> $GITHUB_OUTPUT + NEW_PATCH=$((PATCH + 1)) + NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}" + echo "New version: ${NEW_VERSION}" + echo "new_version=${NEW_VERSION}" >> $GITHUB_OUTPUT build_tag_push: runs-on: ubuntu-latest needs: tag @@ -43,11 +45,11 @@ jobs: uses: gradle/actions/wrapper-validation@v3 - name: Create and push tag run: | - echo "NEW_VERSION=${{ needs.tag.outputs.new_version }}" - git config --global user.name "${{gitea.actor}}" + echo "New version: ${{ needs.tag.outputs.new_version }}" + git config --global user.name "${{ gitea.actor }}" git config --global user.email "${{ gitea.actor }}@users.noreply.github.com" - git tag -a ${{ needs.tag.outputs.new_version }} -m "Pushing new version ${{ needs.tag.outputs.new_version }}" - git push origin ${{ needs.tag.outputs.new_version }} + git tag -a "${{ needs.tag.outputs.new_version }}" -m "Pushing new version ${{ needs.tag.outputs.new_version }}" + git push origin "${{ needs.tag.outputs.new_version }}" - name: Log in to Gitea Docker Registry uses: docker/login-action@v3 with: @@ -62,3 +64,23 @@ jobs: run: | docker push 192.168.0.100:8928/hithomelabs/cftunnels:test docker push 192.168.0.100:8928/hithomelabs/cftunnels:${{ needs.tag.outputs.new_version }} + sync_forks: + name: Sync All Forks + runs-on: ubuntu-latest + needs: build_tag_push + steps: + - name: Check out repository code + uses: actions/checkout@v4 + - name: Sync all forks via Gitea API + run: | + echo "Fetching forks for Hithomelabs/CFTunnels..." + response=$(curl -s -X GET "https://gitea.hithomelabs.com/api/v1/repos/Hithomelabs/CFTunnels/forks" -H "Authorization: token ${{secrets.TOKEN}}") + filtered=$(echo "$response" | grep -o '"clone_url":"[^"]*"' | sed 's/"clone_url":"\([^"]*\)"/\1/' | grep -v "/Hithomelabs") + echo "Detected forks:" + echo "$filtered" + readarray -t forks <<< "$filtered" + for fork_url in "${forks[@]}"; do + echo "🔄 Syncing fork: $fork_url" + authed_url=$(echo "$fork_url" | sed "s#https://#https://${{secrets.TOKEN}}@#") + git push "$authed_url" test & + done diff --git a/build.gradle b/build.gradle index 4328386..0800341 100644 --- a/build.gradle +++ b/build.gradle @@ -15,6 +15,27 @@ java { test { systemProperty 'spring.profiles.active', 'test' + useJUnitPlatform { + excludeTags 'integration' + } + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + exceptionFormat "full" // shows full stack trace + showStandardStreams = true // shows println/log output + } +} + +tasks.register('integrationTestOnly', Test) { + useJUnitPlatform { + includeTags 'integration' + } + description = 'Runs only integration tests tagged with @Tag("integration")' + group = 'verification' + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + showStandardStreams = true + } } repositories { diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 3561707..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'CFTunnels' diff --git a/src/main/java/com/hithomelabs/CFTunnels/Config/CustomOidcUserConfiguration.java b/src/main/java/com/hithomelabs/CFTunnels/Config/CustomOidcUserConfiguration.java index ef077b9..22a4e64 100644 --- a/src/main/java/com/hithomelabs/CFTunnels/Config/CustomOidcUserConfiguration.java +++ b/src/main/java/com/hithomelabs/CFTunnels/Config/CustomOidcUserConfiguration.java @@ -1,7 +1,5 @@ package com.hithomelabs.CFTunnels.Config; -import com.hithomelabs.CFTunnels.Entity.User; -import com.hithomelabs.CFTunnels.Repositories.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.GrantedAuthority; diff --git a/src/main/java/com/hithomelabs/CFTunnels/Config/OpenApiConfig.java b/src/main/java/com/hithomelabs/CFTunnels/Config/OpenApiConfig.java index e24e6f5..1f2af4a 100644 --- a/src/main/java/com/hithomelabs/CFTunnels/Config/OpenApiConfig.java +++ b/src/main/java/com/hithomelabs/CFTunnels/Config/OpenApiConfig.java @@ -5,10 +5,12 @@ import io.swagger.v3.oas.models.servers.Server; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import java.util.ArrayList; @Configuration +@Profile("!integration") public class OpenApiConfig { @Value("${api.baseUrl}") diff --git a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java index c36fd5b..e3ab203 100644 --- a/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java +++ b/src/main/java/com/hithomelabs/CFTunnels/Controllers/TunnelController.java @@ -5,7 +5,6 @@ import com.hithomelabs.CFTunnels.Config.AuthoritiesToGroupMapping; import com.hithomelabs.CFTunnels.Config.CloudflareConfig; import com.hithomelabs.CFTunnels.Config.RestTemplateConfig; import com.hithomelabs.CFTunnels.Entity.Request; -import com.hithomelabs.CFTunnels.Entity.User; import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; import com.hithomelabs.CFTunnels.Models.Config; import com.hithomelabs.CFTunnels.Models.Ingress; @@ -19,12 +18,13 @@ import org.springframework.http.*; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.web.bind.annotation.*; import org.springframework.web.client.RestTemplate; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @RestController @RequestMapping("/cloudflare") @@ -142,7 +142,7 @@ public class TunnelController implements ErrorController { return ResponseEntity.ok(jsonResponse); } -// @PreAuthorize("hasAnyRole('DEVELOPER')") + @PreAuthorize("hasAnyRole('DEVELOPER')") @PutMapping("/tunnel/{tunnelId}/request") public ResponseEntity createTunnelMappingRequest(@PathVariable String tunnelId, @AuthenticationPrincipal OidcUser oidcUser, @RequestBody Ingress ingess){ Request request = mappingRequestService.createMappingRequest(tunnelId, ingess, oidcUser); diff --git a/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java b/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java index 0cf84bb..005a02b 100644 --- a/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java +++ b/src/main/java/com/hithomelabs/CFTunnels/Models/Ingress.java @@ -1,9 +1,17 @@ package com.hithomelabs.CFTunnels.Models; -import java.net.URI; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + import java.util.List; import java.util.Map; +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Setter public class Ingress { private String service; @@ -11,40 +19,8 @@ public class Ingress { private Map originRequest; private String path; - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - public static boolean deleteByHostName(List ingressList, String toBeDeleted){ return ingressList.removeIf(ingress -> ingress.getHostname() != null && ingress.getHostname().equals(toBeDeleted)); } - public String getService() { - return service; - } - - public void setService(String service) { - this.service = service; - } - - public String getHostname() { - return hostname; - } - - public void setHostname(String hostname) { - this.hostname = hostname; - } - - public Map getOriginRequest() { - return originRequest; - } - - public void setOriginRequest(Map originRequest) { - this.originRequest = originRequest; - } } diff --git a/src/main/resources/application-integration.properties b/src/main/resources/application-integration.properties new file mode 100644 index 0000000..055765b --- /dev/null +++ b/src/main/resources/application-integration.properties @@ -0,0 +1,3 @@ +cloudflare.accountId=${CLOUDFLARE_ACCOUNT_ID} +cloudflare.apiKey=${CLOUDFLARE_API_KEY} +cloudflare.email=${CLOUDFLARE_EMAIL} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 89201b8..a0264d2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -4,12 +4,6 @@ cloudflare.apiKey=${CLOUDFLARE_API_KEY} cloudflare.email=${CLOUDFLARE_EMAIL} spring.profiles.active=${ENV} -# set root level -logging.level.root=INFO -# package-specific -logging.level.org.springframework=TRACE -logging.level.com.myapp=INFO - / * * Masking sure app works behind a reverse proxy server.forward-headers-strategy=framework diff --git a/src/test/java/com/hithomelabs/CFTunnels/Integration/CoudflareApiIntegrationTest.java b/src/test/java/com/hithomelabs/CFTunnels/Integration/CoudflareApiIntegrationTest.java new file mode 100644 index 0000000..9553db1 --- /dev/null +++ b/src/test/java/com/hithomelabs/CFTunnels/Integration/CoudflareApiIntegrationTest.java @@ -0,0 +1,107 @@ +package com.hithomelabs.CFTunnels.Integration; + +import com.hithomelabs.CFTunnels.Config.CloudflareConfig; +import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader; +import com.hithomelabs.CFTunnels.Models.Config; +import com.hithomelabs.CFTunnels.Models.Ingress; +import com.hithomelabs.CFTunnels.Models.TunnelResponse; +import com.hithomelabs.CFTunnels.Services.CloudflareAPIService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("integration") +@Tag("integration") +public class CoudflareApiIntegrationTest { + + @Autowired + RestTemplate restTemplate; + + @Autowired + AuthKeyEmailHeader authKeyEmailHeader; + + @Autowired + CloudflareConfig cloudflareConfig; + + @Autowired + CloudflareAPIService cloudflareAPIService; + + private static final String DEV_TUNNEL_ID = "50df9101-f625-4618-b7c5-100338a57124"; + + @Test + @DisplayName("Calls cloudflare cfd tunnels API and checks that dev tunnel should be a part of the response") + public void testGetTunnelsTest() { + + ResponseEntity response = cloudflareAPIService.getCloudflareTunnels(); + + assertEquals(HttpStatus.OK, response.getStatusCode()); + + List> tunnelList = (List>) response.getBody().get("result"); + boolean hasName = tunnelList.stream() + .anyMatch(tunnel -> "devtunnel".equals(tunnel.get("name"))); + + assertTrue(hasName); + } + + @Test + @DisplayName("Calls cloudflare API to get mappings for devtunnel tunnel") + public void testTunnelConfigurations() { + + ResponseEntity responseEntity = cloudflareAPIService.getCloudflareTunnelConfigurations(DEV_TUNNEL_ID, restTemplate, TunnelResponse.class); + + // * * Check if status code is 200 + assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + + // * * Checking if mapping for devdocker exists + TunnelResponse tunnelResponse = responseEntity.getBody(); + boolean hasMatch = tunnelResponse.getResult().getConfig().getIngress().stream() + .anyMatch(ingress -> "devdocker.hithomelabs.com".equals(ingress.getHostname())); + assertTrue(hasMatch); + } + + @Test + @DisplayName("Inserts and deletes a mapping using Cloudflare API") + public void testAddAndDeleteMapping() { + + ResponseEntity beforeMapping = cloudflareAPIService.getCloudflareTunnelConfigurations(DEV_TUNNEL_ID, restTemplate, TunnelResponse.class); + + assertEquals(HttpStatus.OK, beforeMapping.getStatusCode()); + + Ingress ingress = new Ingress(); + ingress.setHostname("random.hithomelabs.com"); + ingress.setService("http://192.168.0.100:3457"); + + Config beforeInsertConfig = beforeMapping.getBody().getResult().getConfig(); + List beforeInsert = beforeInsertConfig.getIngress(); + beforeInsert.add(beforeInsert.size() - 1, ingress); + + ResponseEntity afterInsert = cloudflareAPIService.putCloudflareTunnelConfigurations(DEV_TUNNEL_ID, restTemplate, TunnelResponse.class, beforeInsertConfig); + + assertEquals(HttpStatus.OK, afterInsert.getStatusCode()); + Config afterInsertConfig = afterInsert.getBody().getResult().getConfig(); + List ingressList = afterInsertConfig.getIngress(); + + boolean hasIngress = ingressList.get(ingressList.size() - 2 ).getHostname().equals("random.hithomelabs.com"); + assertTrue(hasIngress); + + Boolean deleteSuccess = Ingress.deleteByHostName(ingressList, ingress.getHostname()); + assertTrue(deleteSuccess); + + ResponseEntity afterDelete = cloudflareAPIService.putCloudflareTunnelConfigurations(DEV_TUNNEL_ID, restTemplate, TunnelResponse.class, afterInsertConfig); + assertEquals(HttpStatus.OK, afterDelete.getStatusCode()); + assertFalse(afterDelete.getBody().getResult().getConfig().getIngress().stream().anyMatch(anyIngress -> "random.hithomelabs.com".equals(anyIngress.getHostname()))); + } + +}