Compare commits

..

9 Commits
0.18.1 ... main

Author SHA1 Message Date
042c706407 Update prod JPA config
All checks were successful
sample gradle build and test / build (pull_request) Successful in 1m46s
sample gradle build and test / tag (push) Successful in 5s
sample gradle build and test / build_tag_push (push) Successful in 2m10s
Daily cloudflare API integration test / cloudflare-api-test (push) Successful in 1m38s
Promote image with tag test to prod / tag (push) Successful in 5s
Promote image with tag test to prod / build_tag_push (push) Successful in 10s
2026-02-16 01:12:32 +05:30
5823d2b6a0 Revert "Update JPA config to use update mode and disable SQL init"
This reverts commit 3fcea268a9.
2026-02-16 01:11:20 +05:30
3fcea268a9 Update JPA config to use update mode and disable SQL init 2026-02-16 01:10:31 +05:30
3c51b761e0 Add RequestRepository integration tests with H2 in-memory database 2026-02-16 00:56:28 +05:30
c78f2713c3 Fix LazyInitializationException and update hostname format
All checks were successful
sample gradle build and test / build (pull_request) Successful in 1m50s
sample gradle build and test / tag (push) Successful in 5s
sample gradle build and test / build_tag_push (push) Successful in 2m0s
2026-02-16 00:38:23 +05:30
3b43039a29 Add Request import and simplify createTestRequest method
All checks were successful
sample gradle build and test / build (pull_request) Successful in 2m2s
sample gradle build and test / tag (push) Successful in 8s
sample gradle build and test / build_tag_push (push) Successful in 2m4s
2026-02-15 22:37:19 +05:30
09e631c871 Restore removed tests for approve/reject endpoints 2026-02-15 22:34:21 +05:30
9a25495d9c Remove pagination and filtering from /requests endpoint 2026-02-15 22:06:07 +05:30
7e3882febf Add PaginationRequest DTO for /requests endpoint
All checks were successful
sample gradle build and test / build (pull_request) Successful in 1m40s
sample gradle build and test / tag (push) Successful in 5s
sample gradle build and test / build_tag_push (push) Successful in 2m13s
2026-02-15 20:51:52 +05:30
15 changed files with 215 additions and 132 deletions

View File

@ -1,11 +0,0 @@
meta {
name: Get tunnels
type: http
seq: 4
}
get {
url: {{base_url}}/cloudflare/tunnels
body: none
auth: none
}

View File

@ -1,11 +0,0 @@
meta {
name: Tunnel
type: http
seq: 5
}
get {
url: {{base_url}}/cloudflare/tunnel/{{tunnel_id}}
body: none
auth: none
}

View File

@ -1,19 +0,0 @@
meta {
name: Write ingress
type: http
seq: 2
}
put {
url: {{base_url}}/cloudflare/tunnel/{{tunnel_id}}/add
body: json
auth: none
}
body:json {
{
"service": "http://192.168.0.100:3457",
"hostname": "random.hithomelabs.com",
"originRequest": {}
}
}

View File

@ -1,9 +0,0 @@
{
"version": "1",
"name": "CFTunnels",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

@ -1,19 +0,0 @@
meta {
name: delete mapping
type: http
seq: 3
}
put {
url: {{base_url}}/cloudflare/tunnel/{{tunnel_id}}/delete
body: json
auth: none
}
body:json {
{
"service": "http://192.168.0.100:6000",
"hostname": "random.hithomelabs.com",
"originRequest": {}
}
}

View File

@ -1,4 +0,0 @@
vars {
tunnel_id: 50df9101-f625-4618-b7c5-100338a57124
base_url: http://localhost:8080
}

View File

@ -1,4 +0,0 @@
vars {
tunnel_id: 50df9101-f625-4618-b7c5-100338a57124
base_url: https://testcf.hithomelabs.com
}

View File

@ -21,8 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.*;
import org.springframework.http.*;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.GrantedAuthority;
@ -109,17 +108,12 @@ public class TunnelController implements ErrorController {
@PreAuthorize("hasAnyRole('USER')")
@GetMapping("/requests")
public ResponseEntity<Map<String,Object>> getAllRequests(
@RequestParam(required = false) Request.RequestStatus status,
Pageable pageable) {
public ResponseEntity<Map<String,Object>> getAllRequests() {
try {
Page<Request> requests = mappingRequestService.getAllRequests(status, pageable);
List<Request> requests = mappingRequestService.getAllRequests();
Map<String, Object> jsonResponse = new HashMap<>();
jsonResponse.put("status", "success");
jsonResponse.put("data", requests.getContent());
jsonResponse.put("currentPage", requests.getNumber());
jsonResponse.put("totalItems", requests.getTotalElements());
jsonResponse.put("totalPages", requests.getTotalPages());
jsonResponse.put("data", requests);
return ResponseEntity.ok(jsonResponse);
} catch (DataAccessException e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

View File

@ -4,11 +4,21 @@ import com.hithomelabs.CFTunnels.Entity.Request;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface RequestRepository extends JpaRepository<Request, UUID> {
Page<Request> findByStatus(Request.RequestStatus status, Pageable pageable);
@Query("SELECT r FROM Request r JOIN FETCH r.mapping m JOIN FETCH m.tunnel JOIN FETCH r.createdBy LEFT JOIN FETCH r.acceptedBy")
List<Request> findAllWithDetails();
@Query("SELECT r FROM Request r JOIN FETCH r.mapping m JOIN FETCH m.tunnel JOIN FETCH r.createdBy LEFT JOIN FETCH r.acceptedBy WHERE r.id = :id")
Optional<Request> findByIdWithDetails(@Param("id") UUID id);
}

View File

@ -11,8 +11,6 @@ import com.hithomelabs.CFTunnels.Repositories.RequestRepository;
import com.hithomelabs.CFTunnels.Repositories.TunnelRepository;
import com.hithomelabs.CFTunnels.Repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Service;
@ -61,11 +59,9 @@ public class MappingRequestService {
return createRequest(mapping, user);
}
public Page<Request> getAllRequests(Request.RequestStatus status, Pageable pageable) {
if (status != null) {
return requestRepository.findByStatus(status, pageable);
}
return requestRepository.findAll(pageable);
@Transactional(readOnly = true)
public List<Request> getAllRequests() {
return requestRepository.findAllWithDetails();
}
public User mapUser(OidcUser oidcUser){
@ -104,7 +100,7 @@ public class MappingRequestService {
@Transactional
public Request approveRequest(UUID requestId, User approver) {
Request request = requestRepository.findById(requestId)
Request request = requestRepository.findByIdWithDetails(requestId)
.orElseThrow(() -> new NoSuchElementException("Request not found"));
if (request.getStatus() != Request.RequestStatus.PENDING) {
@ -132,7 +128,7 @@ public class MappingRequestService {
@Transactional
public Request rejectRequest(UUID requestId, User rejecter) {
Request request = requestRepository.findById(requestId)
Request request = requestRepository.findByIdWithDetails(requestId)
.orElseThrow(() -> new NoSuchElementException("Request not found"));
if (request.getStatus() != Request.RequestStatus.PENDING) {
@ -150,7 +146,7 @@ public class MappingRequestService {
Tunnel tunnel = mapping.getTunnel();
String protocol = mapping.getProtocol().name().toLowerCase();
String service = protocol + "://" + SERVER_IP + ":" + mapping.getPort();
String hostname = mapping.getSubdomain() + "." + tunnel.getName() + ".hithomelabs.com";
String hostname = mapping.getSubdomain() + ".hithomelabs.com";
return new Ingress(service, hostname, null, null);
}
}

View File

@ -7,4 +7,6 @@ management.endpoint.health.show-details=always
logging.level.org.hibernate.SQL=DEBUG
debug=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.datasource.url=jdbc:postgresql://localhost:5432/cftunnel

View File

@ -8,5 +8,5 @@ spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

View File

@ -7,6 +7,6 @@ spring.datasource.password=${POSTGRES_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA Configuration
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

View File

@ -5,6 +5,7 @@ import com.hithomelabs.CFTunnels.Config.AuthoritiesToGroupMapping;
import com.hithomelabs.CFTunnels.Config.CloudflareConfig;
import com.hithomelabs.CFTunnels.Config.RestTemplateConfig;
import com.hithomelabs.CFTunnels.Headers.AuthKeyEmailHeader;
import com.hithomelabs.CFTunnels.Entity.Request;
import com.hithomelabs.CFTunnels.Entity.Tunnel;
import com.hithomelabs.CFTunnels.Models.Authorities;
import com.hithomelabs.CFTunnels.Models.Config;
@ -204,44 +205,22 @@ class TunnelControllerTest {
}
@Test
@DisplayName("should return list of requests with pagination")
@DisplayName("should return list of requests")
void getAllRequests_Success() throws Exception {
List<com.hithomelabs.CFTunnels.Entity.Request> requests = Arrays.asList(
createTestRequest(UUID.randomUUID(), com.hithomelabs.CFTunnels.Entity.Request.RequestStatus.PENDING),
createTestRequest(UUID.randomUUID(), com.hithomelabs.CFTunnels.Entity.Request.RequestStatus.APPROVED)
);
Page<com.hithomelabs.CFTunnels.Entity.Request> page = new PageImpl<>(requests, PageRequest.of(0, 10), 2);
when(mappingRequestService.getAllRequests(any(), any(PageRequest.class))).thenReturn(page);
when(mappingRequestService.getAllRequests()).thenReturn(requests);
mockMvc.perform(get("/cloudflare/requests")
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER)))
.param("page", "0")
.param("size", "10"))
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER))))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value("success"))
.andExpect(jsonPath("$.data").isArray())
.andExpect(jsonPath("$.totalItems").value(2))
.andExpect(jsonPath("$.totalPages").value(1));
}
@Test
@DisplayName("should filter requests by status")
void getAllRequests_WithStatusFilter() throws Exception {
List<com.hithomelabs.CFTunnels.Entity.Request> requests = List.of(
createTestRequest(UUID.randomUUID(), com.hithomelabs.CFTunnels.Entity.Request.RequestStatus.PENDING)
);
Page<com.hithomelabs.CFTunnels.Entity.Request> page = new PageImpl<>(requests, PageRequest.of(0, 10), 1);
when(mappingRequestService.getAllRequests(eq(com.hithomelabs.CFTunnels.Entity.Request.RequestStatus.PENDING), any(PageRequest.class))).thenReturn(page);
mockMvc.perform(get("/cloudflare/requests")
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER)))
.param("status", "PENDING"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("success"))
.andExpect(jsonPath("$.data[0].status").value("PENDING"));
.andExpect(jsonPath("$.data.length()").value(2));
}
@Test
@ -264,13 +243,6 @@ class TunnelControllerTest {
.andExpect(jsonPath("$.status").value("PENDING"));
}
private com.hithomelabs.CFTunnels.Entity.Request createTestRequest(UUID id, com.hithomelabs.CFTunnels.Entity.Request.RequestStatus status) {
com.hithomelabs.CFTunnels.Entity.Request request = new com.hithomelabs.CFTunnels.Entity.Request();
request.setId(id);
request.setStatus(status);
return request;
}
@Test
@DisplayName("should approve mapping request successfully")
void approveMappingRequest_Success() throws Exception {
@ -437,6 +409,13 @@ class TunnelControllerTest {
.andExpect(jsonPath("$.data.result.config.ingress[*].hostname", hasItem("random.hithomelabs.com")));
}
private Request createTestRequest(UUID id, Request.RequestStatus status) {
Request request = new Request();
request.setId(id);
request.setStatus(status);
return request;
}
@Test
void deleteTunnelConfiguration() throws Exception {

View File

@ -0,0 +1,179 @@
package com.hithomelabs.CFTunnels.Repositories;
import com.hithomelabs.CFTunnels.Entity.Mapping;
import com.hithomelabs.CFTunnels.Entity.Protocol;
import com.hithomelabs.CFTunnels.Entity.Request;
import com.hithomelabs.CFTunnels.Entity.Tunnel;
import com.hithomelabs.CFTunnels.Entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@TestPropertySource(properties = {
"spring.jpa.hibernate.ddl-auto=create-drop",
"spring.jpa.show-sql=false",
"spring.sql.init.mode=never"
})
class RequestRepositoryTest {
@Autowired
private RequestRepository requestRepository;
@Autowired
private TunnelRepository tunnelRepository;
@Autowired
private MappingRepository mappingRepository;
@Autowired
private UserRepository userRepository;
private Tunnel tunnel;
private User createdByUser;
private User acceptedByUser;
private Mapping mapping;
@BeforeEach
void setUp() {
tunnel = new Tunnel();
tunnel.setId(UUID.randomUUID());
tunnel.setEnvironment("test");
tunnel.setName("test-tunnel");
tunnel = tunnelRepository.save(tunnel);
createdByUser = new User();
createdByUser.setEmail("creator@example.com");
createdByUser.setName("Creator User");
createdByUser = userRepository.save(createdByUser);
acceptedByUser = new User();
acceptedByUser.setEmail("approver@example.com");
acceptedByUser.setName("Approver User");
acceptedByUser = userRepository.save(acceptedByUser);
mapping = new Mapping();
mapping.setTunnel(tunnel);
mapping.setPort(8080);
mapping.setProtocol(Protocol.HTTP);
mapping.setSubdomain("test-subdomain");
mapping = mappingRepository.save(mapping);
}
@Test
@DisplayName("findAllWithDetails should return requests with all relationships loaded")
void findAllWithDetails_ShouldReturnRequestsWithAllRelationships() {
Request request = new Request();
request.setMapping(mapping);
request.setCreatedBy(createdByUser);
request.setAcceptedBy(acceptedByUser);
request.setStatus(Request.RequestStatus.PENDING);
requestRepository.save(request);
List<Request> results = requestRepository.findAllWithDetails();
assertThat(results).hasSize(1);
Request result = results.get(0);
assertThat(result.getMapping()).isNotNull();
assertThat(result.getMapping().getTunnel()).isNotNull();
assertThat(result.getCreatedBy()).isNotNull();
assertThat(result.getAcceptedBy()).isNotNull();
}
@Test
@DisplayName("findByIdWithDetails should return request with all relationships loaded")
void findByIdWithDetails_ShouldReturnRequestWithAllRelationships() {
Request request = new Request();
request.setMapping(mapping);
request.setCreatedBy(createdByUser);
request.setAcceptedBy(acceptedByUser);
request.setStatus(Request.RequestStatus.PENDING);
UUID requestId = requestRepository.save(request).getId();
Optional<Request> result = requestRepository.findByIdWithDetails(requestId);
assertThat(result).isPresent();
assertThat(result.get().getMapping()).isNotNull();
assertThat(result.get().getMapping().getTunnel()).isNotNull();
assertThat(result.get().getCreatedBy()).isNotNull();
assertThat(result.get().getAcceptedBy()).isNotNull();
}
@Test
@DisplayName("findByIdWithDetails should return empty for non-existent id")
void findByIdWithDetails_ShouldReturnEmptyForNonExistentId() {
Optional<Request> result = requestRepository.findByIdWithDetails(UUID.randomUUID());
assertThat(result).isEmpty();
}
@Test
@DisplayName("findAllWithDetails should return empty list when no requests exist")
void findAllWithDetails_ShouldReturnEmptyListWhenNoRequests() {
List<Request> results = requestRepository.findAllWithDetails();
assertThat(results).isEmpty();
}
@Test
@DisplayName("findAllWithDetails should handle multiple requests with different statuses")
void findAllWithDetails_ShouldHandleMultipleRequests() {
Mapping mapping1 = new Mapping();
mapping1.setTunnel(tunnel);
mapping1.setPort(8080);
mapping1.setProtocol(Protocol.HTTP);
mapping1.setSubdomain("pending-subdomain");
mapping1 = mappingRepository.save(mapping1);
Mapping mapping2 = new Mapping();
mapping2.setTunnel(tunnel);
mapping2.setPort(8081);
mapping2.setProtocol(Protocol.HTTP);
mapping2.setSubdomain("approved-subdomain");
mapping2 = mappingRepository.save(mapping2);
Mapping mapping3 = new Mapping();
mapping3.setTunnel(tunnel);
mapping3.setPort(8082);
mapping3.setProtocol(Protocol.HTTP);
mapping3.setSubdomain("rejected-subdomain");
mapping3 = mappingRepository.save(mapping3);
Request pendingRequest = new Request();
pendingRequest.setMapping(mapping1);
pendingRequest.setCreatedBy(createdByUser);
pendingRequest.setStatus(Request.RequestStatus.PENDING);
requestRepository.save(pendingRequest);
Request approvedRequest = new Request();
approvedRequest.setMapping(mapping2);
approvedRequest.setCreatedBy(createdByUser);
approvedRequest.setAcceptedBy(acceptedByUser);
approvedRequest.setStatus(Request.RequestStatus.APPROVED);
requestRepository.save(approvedRequest);
Request rejectedRequest = new Request();
rejectedRequest.setMapping(mapping3);
rejectedRequest.setCreatedBy(createdByUser);
rejectedRequest.setAcceptedBy(acceptedByUser);
rejectedRequest.setStatus(Request.RequestStatus.REJECTED);
requestRepository.save(rejectedRequest);
List<Request> results = requestRepository.findAllWithDetails();
assertThat(results).hasSize(3);
}
}