Skip to content

Commit 50f8c1e

Browse files
authored
chore: update to Spring Boot 4 and create more e2e tests (#952)
1 parent 4ea9efb commit 50f8c1e

File tree

78 files changed

+4898
-139
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+4898
-139
lines changed

.github/workflows/workflow.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ jobs:
8383
- name: Check code formatting
8484
run: cd e2e && pnpm run format:check
8585

86+
- name: Typecheck E2E
87+
run: cd e2e && pnpm run typecheck
88+
89+
- name: Lint E2E
90+
run: cd e2e && pnpm run lint
91+
8692
- name: Install Playwright Browsers
8793
run: cd e2e && pnpm exec playwright install --with-deps chromium
8894

app/build.gradle

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
plugins {
22
id "java"
33
id "com.diffplug.spotless" version "8.0.0"
4-
id 'org.springframework.boot' version '3.5.6'
4+
id "io.spring.dependency-management" version "1.1.7"
5+
id 'org.springframework.boot' version '4.0.0'
56
}
67

78
repositories {
@@ -14,9 +15,6 @@ java {
1415
}
1516
}
1617

17-
apply plugin: "org.springframework.boot"
18-
apply plugin: "io.spring.dependency-management"
19-
2018
group = "it.chalmers"
2119

2220
dependencies {
@@ -33,29 +31,40 @@ dependencies {
3331
"org.postgresql:postgresql:42.7.3",
3432

3533
// Spring Boot
36-
"org.springframework.boot:spring-boot",
37-
"org.springframework.boot:spring-boot-autoconfigure",
3834
"org.springframework.boot:spring-boot-starter-data-jpa",
35+
"org.springframework.boot:spring-boot-starter-data-redis",
3936
"org.springframework.boot:spring-boot-starter-security",
4037
"org.springframework.boot:spring-boot-starter-validation",
4138
"org.springframework.boot:spring-boot-starter-web",
4239
"org.springframework.boot:spring-boot-starter-oauth2-authorization-server",
4340

4441
// Spring session
45-
"org.springframework.session:spring-session-core:3.5.2",
46-
"org.springframework.session:spring-session-data-redis:3.5.2",
42+
"org.springframework.session:spring-session-core",
43+
"org.springframework.session:spring-session-data-redis",
4744

4845
// Redis
49-
"org.springframework.data:spring-data-redis:3.5.5",
46+
"org.springframework.data:spring-data-redis",
47+
)
48+
49+
compileOnly(
50+
// Testcontainers for local dev service startup
51+
"org.testcontainers:testcontainers:1.21.4",
52+
"org.testcontainers:postgresql:1.21.4",
53+
)
54+
55+
developmentOnly(
56+
// Testcontainers for local dev service startup
57+
"org.testcontainers:testcontainers:1.21.4",
58+
"org.testcontainers:postgresql:1.21.4",
5059
)
5160

5261
runtimeOnly(
5362
// FlywayDB (Database migration)
54-
"org.flywaydb:flyway-core:9.21.0",
63+
"org.springframework.boot:spring-boot-starter-flyway",
64+
"org.flywaydb:flyway-database-postgresql",
5565

5666
// Spring Boot
5767
"org.springframework.boot:spring-boot-devtools",
58-
"org.springframework.boot:spring-boot-starter-data-redis",
5968
"org.springframework.boot:spring-boot-starter-thymeleaf",
6069

6170
// Thymeleaf
@@ -69,6 +78,10 @@ dependencies {
6978
)
7079
}
7180

81+
tasks.named("bootBuildImage") {
82+
environment = ["BP_JVM_VERSION": "25.*"]
83+
}
84+
7285

7386
spotless {
7487
java {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package it.chalmers.gamma.adapter.primary.api;
2+
3+
import it.chalmers.gamma.app.authentication.AccessGuard;
4+
import org.springframework.core.Ordered;
5+
import org.springframework.core.annotation.Order;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.HttpStatusCode;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.web.bind.annotation.ExceptionHandler;
11+
import org.springframework.web.bind.annotation.RestControllerAdvice;
12+
import org.springframework.web.server.ResponseStatusException;
13+
14+
@Order(Ordered.HIGHEST_PRECEDENCE)
15+
@RestControllerAdvice(basePackages = "it.chalmers.gamma.adapter.primary.api")
16+
public class ApiExceptionAdvice {
17+
18+
record ApiErrorBody(int status, String error, String message) {}
19+
20+
@ExceptionHandler(ResponseStatusException.class)
21+
public ResponseEntity<ApiErrorBody> handleResponseStatusException(ResponseStatusException ex) {
22+
return createResponse(ex.getStatusCode(), ex.getReason());
23+
}
24+
25+
@ExceptionHandler(AccessGuard.AccessDeniedException.class)
26+
public ResponseEntity<ApiErrorBody> handleAccessDeniedException(
27+
AccessGuard.AccessDeniedException ex) {
28+
return createResponse(HttpStatus.FORBIDDEN, "FORBIDDEN");
29+
}
30+
31+
private ResponseEntity<ApiErrorBody> createResponse(HttpStatusCode statusCode, String reason) {
32+
HttpStatus status = HttpStatus.resolve(statusCode.value());
33+
String error = status != null ? status.getReasonPhrase() : "Unknown";
34+
String message = reason != null ? reason : error;
35+
ApiErrorBody body = new ApiErrorBody(statusCode.value(), error, message);
36+
37+
return ResponseEntity.status(statusCode).contentType(MediaType.APPLICATION_JSON).body(body);
38+
}
39+
}

app/src/main/java/it/chalmers/gamma/adapter/primary/web/GammaErrorController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import jakarta.servlet.http.HttpServletResponse;
77
import org.slf4j.Logger;
88
import org.slf4j.LoggerFactory;
9-
import org.springframework.boot.web.servlet.error.ErrorController;
9+
import org.springframework.boot.webmvc.error.ErrorController;
1010
import org.springframework.http.HttpStatus;
1111
import org.springframework.stereotype.Controller;
1212
import org.springframework.web.bind.annotation.GetMapping;

app/src/main/java/it/chalmers/gamma/adapter/primary/web/HomeController.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public ModelAndView editMe(
116116
me.groups(),
117117
form.language,
118118
me.isAdmin()));
119+
mv.addObject("gdpr", this.userGdprTrainingFacade.hasGdprTraining(me.id()));
119120

120121
return mv;
121122
}
@@ -129,6 +130,7 @@ public ModelAndView getCancelEdit(
129130

130131
mv.setViewName("home/page :: userinfo");
131132
mv.addObject("me", me);
133+
mv.addObject("gdpr", this.userGdprTrainingFacade.hasGdprTraining(me.id()));
132134

133135
return mv;
134136
}
@@ -185,6 +187,7 @@ public ModelAndView editPassword(
185187

186188
mv.setViewName("home/edited-me-password");
187189
mv.addObject("me", me);
190+
mv.addObject("gdpr", this.userGdprTrainingFacade.hasGdprTraining(me.id()));
188191

189192
return mv;
190193
}

app/src/main/java/it/chalmers/gamma/adapter/primary/web/SuperGroupsController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ public ModelAndView updateSuperGroup(
192192
form.type,
193193
form.svDescription,
194194
form.enDescription));
195+
mv.addObject("usages", this.groupFacade.getAllBySuperGroup(id));
195196

196197
return mv;
197198
}

app/src/main/java/it/chalmers/gamma/adapter/secondary/jpa/apikey/ApiKeyEntity.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class ApiKeyEntity extends MutableEntity<UUID> {
2121
protected String prettyName;
2222

2323
@Enumerated(EnumType.STRING)
24+
@Column(name = "key_type")
2425
protected ApiKeyType keyType;
2526

2627
@JoinColumn(name = "description")

app/src/main/java/it/chalmers/gamma/app/oauth2/GammaAuthorizationService.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@
1010
import it.chalmers.gamma.app.supergroup.domain.SuperGroupId;
1111
import it.chalmers.gamma.app.user.domain.UserId;
1212
import it.chalmers.gamma.app.user.domain.UserMembership;
13+
import java.security.Principal;
1314
import java.util.List;
14-
import org.slf4j.Logger;
15-
import org.slf4j.LoggerFactory;
1615
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
1716
import org.springframework.security.core.userdetails.User;
1817
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
@@ -23,7 +22,6 @@
2322
@Component
2423
public class GammaAuthorizationService implements OAuth2AuthorizationService {
2524

26-
private final Logger LOGGER = LoggerFactory.getLogger(GammaAuthorizationService.class);
2725
private final GammaAuthorizationRepository gammaAuthorizationRepository;
2826
private final ClientRepository clientRepository;
2927
private final GroupRepository groupRepository;
@@ -40,7 +38,7 @@ public GammaAuthorizationService(
4038
@Override
4139
public void save(OAuth2Authorization authorization) {
4240
UsernamePasswordAuthenticationToken authenticationToken =
43-
authorization.getAttribute("java.security.Principal");
41+
authorization.getAttribute(Principal.class.getName());
4442

4543
if (authenticationToken != null && authenticationToken.getPrincipal() instanceof User user) {
4644
Client client =
@@ -56,7 +54,22 @@ public void save(OAuth2Authorization authorization) {
5654
}
5755
}
5856

59-
gammaAuthorizationRepository.save(authorization);
57+
OAuth2Authorization authorizationToSave = authorization;
58+
if (authenticationToken != null) {
59+
UsernamePasswordAuthenticationToken sanitizedAuthenticationToken =
60+
new UsernamePasswordAuthenticationToken(
61+
authenticationToken.getPrincipal(),
62+
authenticationToken.getCredentials(),
63+
authenticationToken.getAuthorities());
64+
sanitizedAuthenticationToken.setDetails(null);
65+
66+
authorizationToSave =
67+
OAuth2Authorization.from(authorization)
68+
.attribute(Principal.class.getName(), sanitizedAuthenticationToken)
69+
.build();
70+
}
71+
72+
gammaAuthorizationRepository.save(authorizationToSave);
6073
}
6174

6275
private boolean userPassesRestriction(ClientRestriction restriction, User user) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package it.chalmers.gamma.bootstrap;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
6+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
7+
import org.springframework.boot.data.redis.autoconfigure.DataRedisConnectionDetails;
8+
import org.springframework.boot.jdbc.autoconfigure.JdbcConnectionDetails;
9+
import org.springframework.context.annotation.Bean;
10+
import org.springframework.context.annotation.Configuration;
11+
import org.testcontainers.containers.GenericContainer;
12+
import org.testcontainers.containers.PostgreSQLContainer;
13+
import org.testcontainers.utility.DockerImageName;
14+
15+
@Configuration(proxyBeanMethods = false)
16+
@ConditionalOnClass(
17+
name = {
18+
"org.testcontainers.containers.PostgreSQLContainer",
19+
"org.testcontainers.containers.GenericContainer"
20+
})
21+
@ConditionalOnProperty(
22+
name = "application.production",
23+
havingValue = "false",
24+
matchIfMissing = true)
25+
@ConditionalOnProperty(
26+
name = "gamma.dev-services.enabled",
27+
havingValue = "true",
28+
matchIfMissing = true)
29+
public class DevServicesConfig {
30+
31+
private static final Logger LOGGER = LoggerFactory.getLogger(DevServicesConfig.class);
32+
private static final DockerImageName POSTGRES_IMAGE = DockerImageName.parse("postgres:16");
33+
private static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:7-alpine");
34+
private static final int REDIS_PORT = 6379;
35+
36+
@Bean(initMethod = "start", destroyMethod = "stop")
37+
public PostgreSQLContainer<?> postgresContainer() {
38+
return new PostgreSQLContainer<>(POSTGRES_IMAGE)
39+
.withDatabaseName("postgres")
40+
.withUsername("postgres")
41+
.withPassword("postgres");
42+
}
43+
44+
@Bean(initMethod = "start", destroyMethod = "stop")
45+
public GenericContainer<?> redisContainer() {
46+
return new GenericContainer<>(REDIS_IMAGE).withExposedPorts(REDIS_PORT);
47+
}
48+
49+
@Bean
50+
public JdbcConnectionDetails jdbcConnectionDetails(PostgreSQLContainer<?> postgresContainer) {
51+
LOGGER.info("Development PostgreSQL container started at {}", postgresContainer.getJdbcUrl());
52+
return new JdbcConnectionDetails() {
53+
@Override
54+
public String getUsername() {
55+
return postgresContainer.getUsername();
56+
}
57+
58+
@Override
59+
public String getPassword() {
60+
return postgresContainer.getPassword();
61+
}
62+
63+
@Override
64+
public String getJdbcUrl() {
65+
return postgresContainer.getJdbcUrl();
66+
}
67+
};
68+
}
69+
70+
@Bean
71+
public DataRedisConnectionDetails dataRedisConnectionDetails(GenericContainer<?> redisContainer) {
72+
LOGGER.info(
73+
"Development Redis container started at {}:{}",
74+
redisContainer.getHost(),
75+
redisContainer.getMappedPort(REDIS_PORT));
76+
return new DataRedisConnectionDetails() {
77+
@Override
78+
public Standalone getStandalone() {
79+
return Standalone.of(redisContainer.getHost(), redisContainer.getMappedPort(REDIS_PORT));
80+
}
81+
};
82+
}
83+
}

app/src/main/java/it/chalmers/gamma/bootstrap/MockBootstrap.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package it.chalmers.gamma.bootstrap;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import java.io.IOException;
54
import org.slf4j.Logger;
65
import org.slf4j.LoggerFactory;
@@ -9,6 +8,7 @@
98
import org.springframework.core.io.Resource;
109
import org.springframework.core.io.ResourceLoader;
1110
import org.springframework.stereotype.Component;
11+
import tools.jackson.databind.ObjectMapper;
1212

1313
@Component
1414
public class MockBootstrap {
@@ -24,18 +24,22 @@ public MockBootstrap(ResourceLoader resourceLoader) {
2424
@Bean
2525
public BootstrapSettings loadBootstrapSettings(
2626
@Value("${application.admin-setup}") boolean adminSetup,
27-
@Value("${application.production}") boolean production) {
28-
return new BootstrapSettings(adminSetup, !production);
27+
@Value("${application.production}") boolean production,
28+
@Value("${application.mocking:false}") boolean isMocking) {
29+
return new BootstrapSettings(adminSetup, !production || isMocking);
2930
}
3031

3132
@Bean
32-
public MockData mockData(BootstrapSettings bootstrapSettings) {
33+
public MockData mockData(
34+
BootstrapSettings bootstrapSettings,
35+
@Value("${application.mock-data-resource:classpath:/mock/mock.json}")
36+
String mockDataResource) {
3337
if (!bootstrapSettings.mocking()) {
3438
LOGGER.info("Not running mock...");
3539
return MockData.empty();
3640
}
3741

38-
Resource resource = this.resourceLoader.getResource("classpath:/mock/mock.json");
42+
Resource resource = this.resourceLoader.getResource(mockDataResource);
3943
ObjectMapper objectMapper = new ObjectMapper();
4044
try {
4145
return objectMapper.readValue(resource.getInputStream(), MockData.class);

0 commit comments

Comments
 (0)