Fixed Auth and Connection issues
Build and Deploy Spring Boot Server / build (push) Successful in 3m41s

This commit is contained in:
2026-07-04 17:31:21 +02:00
parent 7ad72119a8
commit f958fb7853
9 changed files with 353 additions and 9 deletions
@@ -27,7 +27,7 @@ public class ExpenseInput {
private String owner;
@NotNull(message = "Amount is required")
@DecimalMin(value = "0.01", message = "Amount must be greater than zero")
@DecimalMin(value = "0.00", message = "Amount must not be negative")
private Double amount;
private Double personalUseAmount;
@@ -36,7 +36,7 @@ public class ExpenseInput {
@NotNull(message = "Date is required")
private LocalDate date;
@NotBlank(message = "Category is required")
// Optional: Quick Add creates stub expenses without a category, to be filled in later.
private String category;
private ExpenseList expenseList;
@@ -1,6 +1,7 @@
package de.zendric.app.xpensely_server.security;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.Exception.ResourceNotFoundException;
import de.zendric.app.xpensely_server.services.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
@@ -24,12 +25,8 @@ public class AuthenticatedUserResolver {
Jwt jwt = (Jwt) authentication.getPrincipal();
String googleId = jwt.getSubject();
try {
AppUser user = userService.getUserByGoogleId(googleId);
if (user == null) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User not registered");
}
return user;
} catch (IllegalArgumentException e) {
return userService.getUserByGoogleId(googleId);
} catch (IllegalArgumentException | ResourceNotFoundException e) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "User not registered");
}
}
@@ -0,0 +1,24 @@
package de.zendric.app.xpensely_Server.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
/**
* Provides a no-op JwtDecoder so the application context starts without
* fetching Google's JWKS endpoint. Tests use SecurityMockMvcRequestPostProcessors.jwt()
* which populates the SecurityContext directly and never calls this decoder.
*/
@TestConfiguration
public class TestJwtDecoderConfig {
@Bean
@Primary
public JwtDecoder jwtDecoder() {
return token -> {
throw new JwtException("Real JWT validation is disabled in integration tests — use jwt() post-processor");
};
}
}
@@ -1,6 +1,7 @@
package de.zendric.app.xpensely_Server.security;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.model.Exception.ResourceNotFoundException;
import de.zendric.app.xpensely_server.security.AuthenticatedUserResolver;
import de.zendric.app.xpensely_server.services.UserService;
import org.junit.jupiter.api.BeforeEach;
@@ -48,7 +49,8 @@ class AuthenticatedUserResolverTest {
.subject("unknown-id")
.build();
JwtAuthenticationToken auth = new JwtAuthenticationToken(jwt);
when(userService.getUserByGoogleId("unknown-id")).thenReturn(null);
when(userService.getUserByGoogleId("unknown-id"))
.thenThrow(new ResourceNotFoundException("User not found with Google ID: unknown-id"));
ResponseStatusException ex = assertThrows(ResponseStatusException.class,
() -> resolver.resolveCurrentUser(auth));
@@ -0,0 +1,153 @@
package de.zendric.app.xpensely_Server.security;
import de.zendric.app.xpensely_Server.config.TestJwtDecoderConfig;
import de.zendric.app.xpensely_server.model.AppUser;
import de.zendric.app.xpensely_server.repo.ExpenseListRepository;
import de.zendric.app.xpensely_server.repo.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* Full-stack integration tests for the security filter chain.
* Runs with the real securityFilterChain (not the test permit-all chain).
* Uses an H2 in-memory database (see application-integration.properties).
* JWT validation is bypassed via jwt() post-processor — no real Google token needed.
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@ActiveProfiles("integration")
@Import(TestJwtDecoderConfig.class)
class SecurityFilterChainIntegrationTest {
@Autowired MockMvc mockMvc;
@Autowired UserRepository userRepository;
@Autowired ExpenseListRepository expenseListRepository;
private static final String KNOWN_GOOGLE_ID = "google-integration-test-123";
private static final String UNKNOWN_GOOGLE_ID = "google-unknown-999";
@BeforeEach
void setUp() {
expenseListRepository.deleteAll();
userRepository.deleteAll();
AppUser user = new AppUser();
user.setGoogleId(KNOWN_GOOGLE_ID);
user.setUsername("integrationtestuser");
userRepository.save(user);
}
@AfterEach
void tearDown() {
expenseListRepository.deleteAll();
userRepository.deleteAll();
}
// --- Public endpoints ---
@Test
void versionEndpoint_noAuth_returns200() throws Exception {
mockMvc.perform(get("/api/version"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.version").exists());
}
// --- Unauthenticated access (no JWT) → 401 ---
@Test
void byGoogleId_noJwt_returns401() throws Exception {
mockMvc.perform(get("/api/users/byGoogleId").param("id", KNOWN_GOOGLE_ID))
.andExpect(status().isUnauthorized());
}
@Test
void createUser_noJwt_returns401() throws Exception {
mockMvc.perform(post("/api/users/createUser")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser\",\"googleId\":\"new-google-id\"}"))
.andExpect(status().isUnauthorized());
}
@Test
void expenseListMine_noJwt_returns401() throws Exception {
mockMvc.perform(get("/api/expenselist/mine"))
.andExpect(status().isUnauthorized());
}
// --- Authenticated, user exists in DB → success ---
@Test
void byGoogleId_withJwt_userInDb_returns200() throws Exception {
mockMvc.perform(get("/api/users/byGoogleId")
.param("id", KNOWN_GOOGLE_ID)
.with(jwt().jwt(j -> j.subject(KNOWN_GOOGLE_ID))))
.andExpect(status().isOk());
}
@Test
void expenseListMine_withJwt_userInDb_emptyList_returns204() throws Exception {
mockMvc.perform(get("/api/expenselist/mine")
.with(jwt().jwt(j -> j.subject(KNOWN_GOOGLE_ID))))
.andExpect(status().isNoContent());
}
@Test
void createUser_withJwt_validBody_returns201() throws Exception {
mockMvc.perform(post("/api/users/createUser")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"newuser123\",\"googleId\":\"brand-new-google-id\"}")
.with(jwt().jwt(j -> j.subject("brand-new-google-id"))))
.andExpect(status().isCreated());
}
// --- Authenticated, user NOT in DB → 403 ---
@Test
void byGoogleId_withJwt_userNotInDb_returns403() throws Exception {
mockMvc.perform(get("/api/users/byGoogleId")
.param("id", UNKNOWN_GOOGLE_ID)
.with(jwt().jwt(j -> j.subject(UNKNOWN_GOOGLE_ID))))
.andExpect(status().isForbidden());
}
@Test
void expenseListMine_withJwt_userNotInDb_returns403() throws Exception {
mockMvc.perform(get("/api/expenselist/mine")
.with(jwt().jwt(j -> j.subject(UNKNOWN_GOOGLE_ID))))
.andExpect(status().isForbidden());
}
// --- Authenticated, accessing another user's resource → 403 ---
@Test
void byGoogleId_withJwt_differentId_returns403() throws Exception {
mockMvc.perform(get("/api/users/byGoogleId")
.param("id", UNKNOWN_GOOGLE_ID)
.with(jwt().jwt(j -> j.subject(KNOWN_GOOGLE_ID))))
.andExpect(status().isForbidden());
}
// --- Validation errors still return 400 with JWT ---
@Test
void createUser_withJwt_blankUsername_returns400() throws Exception {
mockMvc.perform(post("/api/users/createUser")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"\",\"googleId\":\"some-id\"}")
.with(jwt().jwt(j -> j.subject("some-id"))))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.username").exists());
}
}
@@ -0,0 +1,5 @@
build.artifact=xpensely-server
build.group=de.zendric.app
build.name=XpenselyServer
build.time=2026-01-01T00\:00\:00.000Z
build.version=test
@@ -0,0 +1,10 @@
# H2 in-memory database replaces PostgreSQL for integration tests
spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=PostgreSQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
# Placeholder URI — TestJwtDecoderConfig overrides the decoder bean so this URI is never fetched
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://placeholder.invalid/jwks