CFTunnels/cftunnels-service/src/test/java/com/hithomelabs/cftunnels/Controllers/TunnelControllerTest.java

496 lines
25 KiB
Java

package com.hithomelabs.cftunnels.Controllers;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
import com.hithomelabs.cftunnels.Models.Groups;
import com.hithomelabs.cftunnels.Models.TunnelResponse;
import com.hithomelabs.cftunnels.Models.TunnelResult;
import com.hithomelabs.cftunnels.Models.TunnelsResponse;
import com.hithomelabs.cftunnels.Repositories.UserRepository;
import com.hithomelabs.cftunnels.Services.CloudflareAPIService;
import com.hithomelabs.cftunnels.Services.MappingRequestService;
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.web.servlet.WebMvcTest;
import org.springframework.http.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import static com.hithomelabs.cftunnels.TestUtils.Util.getClassPathDataResource;
import static org.hamcrest.core.IsIterableContaining.hasItem;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.not;
@WebMvcTest(TunnelController.class)
class TunnelControllerTest {
@Autowired
MockMvc mockMvc;
@MockitoBean
AuthoritiesToGroupMapping authoritiesToGroupMapping;
@MockitoBean
CloudflareConfig cloudflareConfig;
@MockitoBean
AuthKeyEmailHeader authKeyEmailHeader;
@MockitoBean
RestTemplate restTemplate;
@MockitoBean
CloudflareAPIService cloudflareAPIService;
@MockitoBean
RestTemplateConfig restTemplateConfig;
@MockitoBean
MappingRequestService mappingRequestService;
@MockitoBean
UserRepository userRepository;
private static final String tunnelResponseSmallIngressFile = "tunnelResponseSmallIngress.json";
private static final String tunnelResponseLargeIngressFile = "tunnelResponseLargeIngress.json";
private static final String withAdditionalIngress;
static {
try {
withAdditionalIngress = getClassPathDataResource(tunnelResponseLargeIngressFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static final String withoutAdditionalIngress;
static {
try {
withoutAdditionalIngress = getClassPathDataResource(tunnelResponseSmallIngressFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static final String ingressJson = """
{
"service": "http://192.168.0.100:3457",
"hostname": "random.hithomelabs.com",
"originRequest": {}
}
""";
private DefaultOidcUser buildOidcUser(String username, String role) {
when(authoritiesToGroupMapping.getAuthorityForGroup()).thenReturn(Map.of(Groups.GITEA_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))),
Groups.POWER_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))),
Groups.HOMELAB_DEVELOPER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_DEVELOPER))),
Groups.SYSTEM_ADMIN, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_APPROVER), new SimpleGrantedAuthority(Authorities.ROLE_ADMIN)))));
Map<String, Set<GrantedAuthority>> roleAuthorityMapping = authoritiesToGroupMapping.getAuthorityForGroup();
List<GrantedAuthority> authorities = roleAuthorityMapping.get(role).stream().toList();
OidcIdToken idToken = new OidcIdToken(
"mock-token",
Instant.now(),
Instant.now().plusSeconds(3600),
Map.of("preferred_username", username, "sub", username)
);
return new DefaultOidcUser(authorities, idToken, "preferred_username");
}
private DefaultOidcUser buildOidcUserWithEmail(String username, String role, String email) {
when(authoritiesToGroupMapping.getAuthorityForGroup()).thenReturn(Map.of(Groups.GITEA_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))),
Groups.POWER_USER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_USER))),
Groups.HOMELAB_DEVELOPER, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_DEVELOPER))),
Groups.SYSTEM_ADMIN, new HashSet<>(Set.of(new SimpleGrantedAuthority(Authorities.ROLE_APPROVER), new SimpleGrantedAuthority(Authorities.ROLE_ADMIN)))));
Map<String, Set<GrantedAuthority>> roleAuthorityMapping = authoritiesToGroupMapping.getAuthorityForGroup();
List<GrantedAuthority> authorities = roleAuthorityMapping.get(role).stream().toList();
OidcIdToken idToken = new OidcIdToken(
"mock-token",
Instant.now(),
Instant.now().plusSeconds(3600),
Map.of("preferred_username", username, "sub", username, "email", email)
);
return new DefaultOidcUser(authorities, idToken, "preferred_username");
}
@Test
@DisplayName("should return appropriate user roles when use belongs to group GITEA_USER")
public void testWhoAmI_user() throws Exception {
mockMvc.perform(get("/cloudflare/whoami")
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER))))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.username").value("username"))
.andExpect(jsonPath("$.roles", hasItem("ROLE_USER")));
}
@Test
@DisplayName("should hit tunnels endpoint successfully with ROLE_USER")
public void testGetTunnelsForRoleUser() throws Exception {
when(cloudflareConfig.getAccountId()).thenReturn("abc123");
HttpHeaders headers = new HttpHeaders();
headers.set("X-Auth-Email", "me@example.com");
when(authKeyEmailHeader.getHttpHeaders()).thenReturn(headers);
List<TunnelResult> tunnelResults = List.of(new TunnelResult("50df9101-f625-4618-b7c5-100338a57124", "test-tunnel"));
TunnelsResponse tunnelsResponse = new TunnelsResponse(tunnelResults, null, null, true);
ResponseEntity<TunnelsResponse> mockResponse = new ResponseEntity<>(tunnelsResponse, HttpStatus.OK);
when(cloudflareAPIService.getCloudflareTunnels()).thenReturn(mockResponse);
mockMvc.perform(get("/cloudflare/tunnels")
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.GITEA_USER))))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.result[0].id").value("50df9101-f625-4618-b7c5-100338a57124"));
}
@Test
@DisplayName("should return list of configured tunnels from database")
void getConfiguredTunnels() throws Exception {
List<Tunnel> tunnels = List.of(
new Tunnel(UUID.fromString("50df9101-f625-4618-b7c5-100338a57124"), "dev", "devtunnel"),
new Tunnel(UUID.fromString("60df9101-f625-4618-b7c5-100338a57125"), "prod", "prodtunnel")
);
when(cloudflareAPIService.getAllConfiguredTunnels()).thenReturn(tunnels);
mockMvc.perform(get("/cloudflare/configured/tunnels")
.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[*].name", hasItem("devtunnel")))
.andExpect(jsonPath("$.data[*].name", hasItem("prodtunnel")));
}
@Test
@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)
);
when(mappingRequestService.getAllRequests()).thenReturn(requests);
mockMvc.perform(get("/cloudflare/requests")
.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("$.data.length()").value(2));
}
@Test
@DisplayName("should create mapping request successfully")
void createTunnelMappingRequest_Success() throws Exception {
UUID tunnelId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.Request createdRequest = new com.hithomelabs.cftunnels.Entity.Request();
createdRequest.setId(UUID.randomUUID());
createdRequest.setStatus(com.hithomelabs.cftunnels.Entity.Request.RequestStatus.PENDING);
when(mappingRequestService.createMappingRequest(any(String.class), any(com.hithomelabs.cftunnels.Models.Ingress.class), any())).thenReturn(createdRequest);
mockMvc.perform(post("/cloudflare/tunnels/configure/{tunnelId}/requests", tunnelId.toString())
.with(oauth2Login().oauth2User(buildOidcUser("developer", Groups.HOMELAB_DEVELOPER)))
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(ingressJson))
.andExpect(status().isCreated())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value("PENDING"));
}
@Test
@DisplayName("should approve mapping request successfully")
void approveMappingRequest_Success() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User approverUser = new com.hithomelabs.cftunnels.Entity.User();
approverUser.setEmail("approver@example.com");
approverUser.setName("Approver");
com.hithomelabs.cftunnels.Entity.Request approvedRequest = new com.hithomelabs.cftunnels.Entity.Request();
approvedRequest.setId(requestId);
approvedRequest.setStatus(com.hithomelabs.cftunnels.Entity.Request.RequestStatus.APPROVED);
when(mappingRequestService.approveRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenReturn(approvedRequest);
when(userRepository.findByEmail("approver@example.com"))
.thenReturn(java.util.Optional.of(approverUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/approve", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("approver", Groups.SYSTEM_ADMIN, "approver@example.com")))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value("APPROVED"));
}
@Test
@DisplayName("should return 404 when request not found")
void approveMappingRequest_NotFound() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User approverUser = new com.hithomelabs.cftunnels.Entity.User();
approverUser.setEmail("approver@example.com");
approverUser.setName("Approver");
when(mappingRequestService.approveRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenThrow(new NoSuchElementException("Request not found"));
when(userRepository.findByEmail("approver@example.com"))
.thenReturn(java.util.Optional.of(approverUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/approve", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("approver", Groups.SYSTEM_ADMIN, "approver@example.com")))
.with(csrf()))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("should return 500 when mapping creation fails")
void approveMappingRequest_InternalServerError() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User approverUser = new com.hithomelabs.cftunnels.Entity.User();
approverUser.setEmail("approver@example.com");
approverUser.setName("Approver");
when(mappingRequestService.approveRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenThrow(new RuntimeException("Failed to add mapping to Cloudflare"));
when(userRepository.findByEmail("approver@example.com"))
.thenReturn(java.util.Optional.of(approverUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/approve", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("approver", Groups.SYSTEM_ADMIN, "approver@example.com")))
.with(csrf()))
.andExpect(status().isInternalServerError());
}
@Test
@DisplayName("should reject mapping request successfully")
void rejectMappingRequest_Success() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User rejecterUser = new com.hithomelabs.cftunnels.Entity.User();
rejecterUser.setEmail("rejecter@example.com");
rejecterUser.setName("Rejecter");
com.hithomelabs.cftunnels.Entity.Request rejectedRequest = new com.hithomelabs.cftunnels.Entity.Request();
rejectedRequest.setId(requestId);
rejectedRequest.setStatus(com.hithomelabs.cftunnels.Entity.Request.RequestStatus.REJECTED);
when(mappingRequestService.rejectRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenReturn(rejectedRequest);
when(userRepository.findByEmail("rejecter@example.com"))
.thenReturn(java.util.Optional.of(rejecterUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/reject", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("rejecter", Groups.SYSTEM_ADMIN, "rejecter@example.com")))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.status").value("REJECTED"));
}
@Test
@DisplayName("should return 404 when rejecting non-existent request")
void rejectMappingRequest_NotFound() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User rejecterUser = new com.hithomelabs.cftunnels.Entity.User();
rejecterUser.setEmail("rejecter@example.com");
rejecterUser.setName("Rejecter");
when(mappingRequestService.rejectRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenThrow(new NoSuchElementException("Request not found"));
when(userRepository.findByEmail("rejecter@example.com"))
.thenReturn(java.util.Optional.of(rejecterUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/reject", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("rejecter", Groups.SYSTEM_ADMIN, "rejecter@example.com")))
.with(csrf()))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("should return 409 when rejecting already processed request")
void rejectMappingRequest_Conflict() throws Exception {
UUID requestId = UUID.randomUUID();
com.hithomelabs.cftunnels.Entity.User rejecterUser = new com.hithomelabs.cftunnels.Entity.User();
rejecterUser.setEmail("rejecter@example.com");
rejecterUser.setName("Rejecter");
when(mappingRequestService.rejectRequest(eq(requestId), any(com.hithomelabs.cftunnels.Entity.User.class)))
.thenThrow(new IllegalStateException("Request is not in PENDING status"));
when(userRepository.findByEmail("rejecter@example.com"))
.thenReturn(java.util.Optional.of(rejecterUser));
mockMvc.perform(put("/cloudflare/requests/{requestId}/reject", requestId)
.with(oauth2Login().oauth2User(buildOidcUserWithEmail("rejecter", Groups.SYSTEM_ADMIN, "rejecter@example.com")))
.with(csrf()))
.andExpect(status().isConflict());
}
@Test
void getTunnelConfigurations() throws Exception {
Map<String, Object> tunnelData = Map.of("config", Map.of("result", "success", "ingress", "sample ingress object"));
ResponseEntity<Map> mockResponse = new ResponseEntity<>(tunnelData, HttpStatus.OK);
when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(Map.class))).thenReturn(mockResponse);
mockMvc.perform(get("/cloudflare/tunnels/{tunnelId}/mappings", "sampleTunnelId")
.with(oauth2Login().oauth2User(buildOidcUser("username", Groups.HOMELAB_DEVELOPER))))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.config.ingress").value("sample ingress object"));
}
@Test
void addTunnelconfiguration() throws Exception {
when(restTemplateConfig.restTemplate()).thenReturn(new RestTemplate());
ObjectMapper mapper = new ObjectMapper();
TunnelResponse tunnelStateBefore = mapper.readValue(withoutAdditionalIngress, TunnelResponse.class);
ResponseEntity<TunnelResponse> tunnelResponseBefore = new ResponseEntity<>(tunnelStateBefore, HttpStatus.OK);
when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class))).thenReturn(tunnelResponseBefore);
TunnelResponse expectedTunnelConfig = mapper.readValue(withAdditionalIngress, TunnelResponse.class);
ResponseEntity<TunnelResponse> expectedHttpTunnelResponse = new ResponseEntity<>(expectedTunnelConfig, HttpStatus.OK);
when(cloudflareAPIService.putCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class), any(Config.class))).thenReturn(expectedHttpTunnelResponse);
mockMvc.perform(post("/cloudflare/tunnels/{tunnelId}/mappings", "sampleTunnelId")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(ingressJson))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.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 {
when(restTemplateConfig.restTemplate()).thenReturn(new RestTemplate());
ObjectMapper mapper = new ObjectMapper();
TunnelResponse tunnelStateBefore = mapper.readValue(withAdditionalIngress, TunnelResponse.class);
ResponseEntity<TunnelResponse> tunnelResponseBefore = new ResponseEntity<>(tunnelStateBefore, HttpStatus.OK);
when(cloudflareAPIService.getCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class))).thenReturn(tunnelResponseBefore);
TunnelResponse expectedTunnelConfig = mapper.readValue(withoutAdditionalIngress, TunnelResponse.class);
ResponseEntity<TunnelResponse> expectedHttpTunnelResponse = new ResponseEntity<>(expectedTunnelConfig, HttpStatus.OK);
when(cloudflareAPIService.putCloudflareTunnelConfigurations(eq("sampleTunnelId"), any(RestTemplate.class), eq(TunnelResponse.class), any(Config.class))).thenReturn(expectedHttpTunnelResponse);
mockMvc.perform(delete("/cloudflare/tunnels/{tunnelId}/mappings", "sampleTunnelId")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(ingressJson))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.data.result.config.ingress[*].hostname", not(hasItem("random.hithomelabs.com"))));
}
@Test
@DisplayName("should return 200 OK with tunnel when tunnel is successfully updated")
void configureTunnelForEnvironment_Success() throws Exception {
Tunnel tunnel = new Tunnel(UUID.randomUUID(), "dev", "test-tunnel");
when(cloudflareAPIService.createOrUpdateTunnel(eq("test-tunnel-id"), any(String.class))).thenReturn(tunnel);
mockMvc.perform(put("/cloudflare/tunnels/configure/{tunnelId}", "test-tunnel-id")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf()))
.andExpect(status().isOk())
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.name").value("test-tunnel"))
.andExpect(jsonPath("$.environment").value("dev"));
}
@Test
@DisplayName("should return 204 NO_CONTENT when tunnel does not need changes")
void configureTunnelForEnvironment_NoContent() throws Exception {
when(cloudflareAPIService.createOrUpdateTunnel(eq("test-tunnel-id"), any(String.class))).thenReturn(null);
mockMvc.perform(put("/cloudflare/tunnels/configure/{tunnelId}", "test-tunnel-id")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf()))
.andExpect(status().isNoContent());
}
@Test
@DisplayName("should return 404 NOT_FOUND when tunnelId is not valid")
void configureTunnelForEnvironment_NotFound() throws Exception {
when(cloudflareAPIService.createOrUpdateTunnel(eq("invalid-tunnel-id"), any(String.class)))
.thenThrow(new NoSuchElementException("Tunnel not found"));
mockMvc.perform(put("/cloudflare/tunnels/configure/{tunnelId}", "invalid-tunnel-id")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf()))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("should return 500 INTERNAL_SERVER_ERROR when runtime exception occurs")
void configureTunnelForEnvironment_InternalServerError() throws Exception {
when(cloudflareAPIService.createOrUpdateTunnel(eq("test-tunnel-id"), any(String.class)))
.thenThrow(new RuntimeException("Internal error"));
mockMvc.perform(put("/cloudflare/tunnels/configure/{tunnelId}", "test-tunnel-id")
.with(oauth2Login().oauth2User(buildOidcUser("admin", Groups.SYSTEM_ADMIN)))
.with(csrf()))
.andExpect(status().isInternalServerError());
}
}