Good stuff

This commit is contained in:
Tim Zöller 2025-12-05 10:21:45 +01:00
parent 9e428a0499
commit 0e81a65d62
11 changed files with 86 additions and 59 deletions

View file

@ -5,11 +5,9 @@ import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.operaton.fitpub.config.TestcontainersConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.client.RestTemplate;
@ -22,7 +20,6 @@ import org.springframework.web.client.RestTemplate;
@SpringBootApplication
@EnableAsync
@Slf4j
@Import(TestcontainersConfiguration.class)
public class FitPubApplication {
public static void main(String[] args) {

View file

@ -1,42 +0,0 @@
package org.operaton.fitpub.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Testcontainers configuration for development using Spring Boot Dev Services.
* Automatically starts a PostgreSQL container with PostGIS extension when running in dev mode.
*
* This ensures development environment matches production (PostgreSQL + PostGIS).
*
* Only active when NOT running in production profile AND Testcontainers is on the classpath.
*/
@Configuration(proxyBeanMethods = false)
@Profile("!prod")
@ConditionalOnClass(PostgreSQLContainer.class)
public class TestcontainersConfiguration {
/**
* PostgreSQL container with PostGIS extension.
* Uses postgis/postgis image which includes both PostgreSQL and PostGIS.
*
* @ServiceConnection automatically configures spring.datasource.* properties
*/
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres")
)
.withDatabaseName("fitpub")
.withUsername("fitpub")
.withPassword("fitpub")
.withReuse(true); // Reuse container across runs for faster startup
}
}

View file

@ -68,7 +68,7 @@ public class Actor {
.mediaType("image/jpeg")
.url(user.getAvatarUrl())
.build() : null)
.url(baseUrl + "/@" + user.getUsername())
.url(baseUrl + "/users/" + user.getUsername())
.build();
}

View file

@ -0,0 +1,6 @@
# Exclude Testcontainers from devtools restart
# This allows Testcontainers to be loaded by the base classloader
restart.exclude.testcontainers=/testcontainers[\\w\\-]*\\.jar
restart.exclude.testcontainers-postgresql=/testcontainers-postgresql[\\w\\-]*\\.jar
restart.exclude.docker-java=/docker-java[\\w\\-]*\\.jar
restart.exclude.duct-tape=/duct-tape[\\w\\-]*\\.jar

View file

@ -3,9 +3,10 @@
spring:
datasource:
url: jdbc:postgresql://localhost:5432/fitpub
username: fitpub
password: change_me_in_production
# For dev: Start PostgreSQL with: docker run -d --name fitpub-postgres -p 5432:5432 -e POSTGRES_DB=fitpub -e POSTGRES_USER=fitpub -e POSTGRES_PASSWORD=fitpub postgis/postgis:16-3.4
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/fitpub}
username: ${SPRING_DATASOURCE_USERNAME:fitpub}
password: ${SPRING_DATASOURCE_PASSWORD:fitpub}
driver-class-name: org.postgresql.Driver
jpa:

View file

@ -266,11 +266,19 @@
#activityTypeTabs .nav-link {
cursor: pointer;
color: var(--dark-color) !important;
font-weight: 700;
transition: all 0.3s ease;
}
#activityTypeTabs .nav-link:hover {
color: var(--primary-color) !important;
background-color: rgba(255, 20, 147, 0.1);
}
#activityTypeTabs .nav-link.active {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
color: white !important;
border-color: var(--primary-color);
}
</style>

View file

@ -29,13 +29,13 @@
<!-- Period Selector -->
<div class="btn-group mb-4 w-100" role="group">
<button type="button" class="btn btn-outline-primary active" onclick="loadTrainingLoad(30)">
<button type="button" class="btn btn-outline-primary active" onclick="loadTrainingLoad(event, 30)">
Last 30 Days
</button>
<button type="button" class="btn btn-outline-primary" onclick="loadTrainingLoad(60)">
<button type="button" class="btn btn-outline-primary" onclick="loadTrainingLoad(event, 60)">
Last 60 Days
</button>
<button type="button" class="btn btn-outline-primary" onclick="loadTrainingLoad(90)">
<button type="button" class="btn btn-outline-primary" onclick="loadTrainingLoad(event, 90)">
Last 90 Days
</button>
</div>
@ -123,11 +123,13 @@
card.style.backgroundColor = config.bgColor;
}
async function loadTrainingLoad(days = 30) {
async function loadTrainingLoad(event, days = 30) {
try {
// Update active button
document.querySelectorAll('.btn-group button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Update active button (only if called from a button click)
if (event && event.target) {
document.querySelectorAll('.btn-group button').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
}
document.getElementById('loading-spinner').style.display = 'block';
document.getElementById('charts-content').style.display = 'none';
@ -301,7 +303,7 @@
return;
}
loadFormStatus();
loadTrainingLoad(30);
loadTrainingLoad(null, 30);
});
</script>
</div>

View file

@ -0,0 +1,32 @@
package org.operaton.fitpub.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Testcontainers configuration for tests.
* Automatically starts a PostgreSQL container with PostGIS extension for integration tests.
*/
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfiguration {
/**
* PostgreSQL container with PostGIS extension for tests.
* @ServiceConnection automatically configures the datasource from this container.
*/
@Bean
@ServiceConnection
public PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>(
DockerImageName.parse("postgis/postgis:16-3.4")
.asCompatibleSubstituteFor("postgres")
)
.withDatabaseName("fitpub")
.withUsername("fitpub")
.withPassword("fitpub")
.withReuse(true);
}
}

View file

@ -2,10 +2,12 @@ package org.operaton.fitpub.security;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.operaton.fitpub.config.TestcontainersConfiguration;
import org.operaton.fitpub.model.entity.User;
import org.operaton.fitpub.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
@ -23,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.*;
* Test to validate that users' public and private keys match.
*/
@SpringBootTest
@Import(TestcontainersConfiguration.class)
@Slf4j
public class KeyPairValidationTest {

View file

@ -3,6 +3,7 @@ package org.operaton.fitpub.service;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.operaton.fitpub.config.TestcontainersConfiguration;
import org.operaton.fitpub.model.entity.Activity;
import org.operaton.fitpub.model.entity.ActivityMetrics;
import org.operaton.fitpub.model.entity.User;
@ -11,6 +12,7 @@ import org.operaton.fitpub.repository.UserRepository;
import org.operaton.fitpub.util.FitParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import java.io.File;
@ -30,6 +32,7 @@ import static org.junit.jupiter.api.Assertions.*;
"fitpub.image.osm-tiles.enabled=true"
})
@ActiveProfiles("test")
@Import(TestcontainersConfiguration.class)
class ActivityImageServiceTest {
@Autowired

View file

@ -55,6 +55,21 @@ class FitFileServiceTest {
@Mock
private ActivityMetricsRepository metricsRepository;
@Mock
private PersonalRecordService personalRecordService;
@Mock
private AchievementService achievementService;
@Mock
private TrainingLoadService trainingLoadService;
@Mock
private ActivitySummaryService activitySummaryService;
@Mock
private WeatherService weatherService;
@Spy
private ObjectMapper objectMapper;
@ -148,7 +163,9 @@ class FitFileServiceTest {
// Assert
assertNotNull(result);
assertTrue(result.getTitle().contains("Run"));
assertTrue(result.getTitle().contains(testParsedData.getStartTime().toLocalDate().toString()));
// Title format is "[Time of Day] [Activity Type]" (e.g., "Morning Run")
// Start time is 8:00 AM, which is "Morning"
assertTrue(result.getTitle().contains("Morning"));
}
@Test