coordinates = new java.util.ArrayList<>();
- for (Object coordinateObj : coordinateList) {
- Coordinate coordinate = parseCoordinate(coordinateObj);
- if (coordinate == null) {
- return null;
- }
- coordinates.add(coordinate);
- }
-
- if (coordinates.size() < 2) {
- return null;
- }
-
- return GEOMETRY_FACTORY.createLineString(coordinates.toArray(new Coordinate[0]));
- }
-
- private Coordinate parseCoordinate(Object coordinateObj) {
- if (!(coordinateObj instanceof java.util.List> coordinateValues) || coordinateValues.size() < 2) {
- return null;
- }
-
- Double longitude = parseDouble(coordinateValues.get(0));
- Double latitude = parseDouble(coordinateValues.get(1));
- if (longitude == null || latitude == null) {
- return null;
- }
-
- return new Coordinate(longitude, latitude);
- }
-
- private String firstNonBlank(String... values) {
- for (String value : values) {
- if (value != null && !value.isBlank()) {
- return value;
- }
- }
- return null;
- }
-
/**
* Extract attachment URLs (map image, GeoJSON) from a Note object.
*/
@@ -922,44 +824,6 @@ public class InboxProcessor {
}
}
- /**
- * Parse ActivityPub published timestamps.
- *
- * Preferred input is a full ISO-8601 instant with timezone/offset. Some
- * remote implementations still send zoneless timestamps, so we accept those
- * as a compatibility fallback and interpret them as UTC.
- */
- private Instant parsePublishedAt(String publishedStr) {
- if (publishedStr == null || publishedStr.isBlank()) {
- return Instant.now();
- }
-
- try {
- return Instant.parse(publishedStr);
- } catch (DateTimeParseException ignored) {
- // Fall through to compatibility parsers below.
- }
-
- try {
- return OffsetDateTime.parse(publishedStr).toInstant();
- } catch (DateTimeParseException ignored) {
- // Fall through to compatibility parsers below.
- }
-
- try {
- return ZonedDateTime.parse(publishedStr).toInstant();
- } catch (DateTimeParseException ignored) {
- // Fall through to compatibility parsers below.
- }
-
- try {
- return LocalDateTime.parse(publishedStr).atOffset(ZoneOffset.UTC).toInstant();
- } catch (DateTimeParseException e) {
- log.warn("Failed to parse published timestamp: {}", publishedStr, e);
- return Instant.now();
- }
- }
-
/**
* Serialize object to JSON string.
*/
diff --git a/src/main/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilder.java b/src/main/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilder.java
deleted file mode 100644
index dd8752d..0000000
--- a/src/main/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilder.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.javahippie.fitpub.service;
-
-import lombok.RequiredArgsConstructor;
-import net.javahippie.fitpub.model.dto.ActivityDTO;
-import net.javahippie.fitpub.model.entity.Activity;
-import net.javahippie.fitpub.model.entity.ActivityMetrics;
-import net.javahippie.fitpub.model.entity.PrivacyZone;
-import org.springframework.stereotype.Service;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Builds the proprietary workoutData payload for outbound ActivityPub Notes.
- */
-@Service
-@RequiredArgsConstructor
-public class WorkoutDataPayloadBuilder {
-
- private final PrivacyZoneService privacyZoneService;
- private final TrackPrivacyFilter trackPrivacyFilter;
-
- public Map build(Activity activity) {
- Map workoutData = new HashMap<>();
- workoutData.put("activityType", activity.getActivityType().name());
-
- if (activity.getDescription() != null && !activity.getDescription().isBlank()) {
- workoutData.put("description", activity.getDescription());
- }
- if (activity.getTotalDistance() != null) {
- workoutData.put("distance", activity.getTotalDistance().longValue());
- }
- if (activity.getTotalDurationSeconds() != null) {
- workoutData.put("duration", Duration.ofSeconds(activity.getTotalDurationSeconds()).toString());
- }
- if (activity.getElevationGain() != null) {
- workoutData.put("elevationGain", activity.getElevationGain().intValue());
- }
-
- ActivityMetrics metrics = activity.getMetrics();
- if (metrics != null) {
- if (metrics.getAveragePaceSeconds() != null) {
- workoutData.put("averagePace", Duration.ofSeconds(metrics.getAveragePaceSeconds()).toString());
- }
- if (metrics.getAverageHeartRate() != null) {
- workoutData.put("averageHeartRate", metrics.getAverageHeartRate());
- }
- if (metrics.getAverageSpeed() != null) {
- workoutData.put("averageSpeed", metrics.getAverageSpeed().doubleValue());
- }
- if (metrics.getMaxSpeed() != null) {
- workoutData.put("maxSpeed", metrics.getMaxSpeed().doubleValue());
- }
- if (metrics.getCalories() != null) {
- workoutData.put("calories", metrics.getCalories());
- }
- }
-
- Map route = buildRoutePayload(activity);
- if (route != null) {
- workoutData.put("route", route);
- }
-
- return workoutData;
- }
-
- private Map buildRoutePayload(Activity activity) {
- List privacyZones = privacyZoneService.getActivePrivacyZones(activity.getUserId());
- ActivityDTO dto = ActivityDTO.fromEntityWithFiltering(activity, null, privacyZones, trackPrivacyFilter);
-
- if (dto.getSimplifiedTrack() == null) {
- return null;
- }
-
- Map feature = new HashMap<>();
- feature.put("type", "Feature");
- feature.put("geometry", dto.getSimplifiedTrack());
-
- Map featureCollection = new HashMap<>();
- featureCollection.put("type", "FeatureCollection");
- featureCollection.put("features", List.of(feature));
- return featureCollection;
- }
-}
diff --git a/src/main/java/net/javahippie/fitpub/util/ActivityFormatter.java b/src/main/java/net/javahippie/fitpub/util/ActivityFormatter.java
index 0b32b3d..26e4f32 100644
--- a/src/main/java/net/javahippie/fitpub/util/ActivityFormatter.java
+++ b/src/main/java/net/javahippie/fitpub/util/ActivityFormatter.java
@@ -98,10 +98,6 @@ public class ActivityFormatter {
*
*/
private static LocalDateTime getUtcDateTimeInZone(LocalDateTime utcDateTime, String timezone) {
- if (timezone == null || timezone.isBlank()) {
- return utcDateTime;
- }
-
try {
return utcDateTime.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of(timezone))
diff --git a/src/main/java/net/javahippie/fitpub/util/ActivityPubContexts.java b/src/main/java/net/javahippie/fitpub/util/ActivityPubContexts.java
index ce424c6..84581bd 100644
--- a/src/main/java/net/javahippie/fitpub/util/ActivityPubContexts.java
+++ b/src/main/java/net/javahippie/fitpub/util/ActivityPubContexts.java
@@ -35,8 +35,7 @@ public final class ActivityPubContexts {
/**
* Returns the extended JSON-LD {@code @context} value for outbound objects
- * that carry both interaction-policy declarations and FitPub's proprietary
- * {@code workoutData} extension fields. Shape:
+ * that carry interaction-policy declarations. Shape:
*
*
* [
@@ -46,20 +45,7 @@ public final class ActivityPubContexts {
* "interactionPolicy": { "@id": "gts:interactionPolicy", "@type": "@id" },
* "canQuote": { "@id": "gts:canQuote", "@type": "@id" },
* "automaticApproval": { "@id": "gts:automaticApproval", "@type": "@id" },
- * "manualApproval": { "@id": "gts:manualApproval", "@type": "@id" },
- * "fitpub": "https://fitpub.social/ns#",
- * "workoutData": "fitpub:workoutData",
- * "activityType": "fitpub:activityType",
- * "description": "fitpub:description",
- * "distance": "fitpub:distance",
- * "duration": "fitpub:duration",
- * "elevationGain": "fitpub:elevationGain",
- * "averagePace": "fitpub:averagePace",
- * "averageHeartRate": "fitpub:averageHeartRate",
- * "averageSpeed": "fitpub:averageSpeed",
- * "maxSpeed": "fitpub:maxSpeed",
- * "calories": "fitpub:calories",
- * "route": "fitpub:route"
+ * "manualApproval": { "@id": "gts:manualApproval", "@type": "@id" }
* }
* ]
*
@@ -70,12 +56,6 @@ public final class ActivityPubContexts {
* Mastodon source, "interaction_policies" extension), so a Mastodon
* receiver compacting our object with its own context will recognise the
* field names and apply the policy.
- *
- * The {@code fitpub:} prefix is FitPub's own extension namespace
- * ({@code https://fitpub.social/ns#}). It declares the proprietary
- * {@code workoutData} object and its structured activity fields so FitPub
- * instances can exchange machine-readable workout metadata without
- * overloading the standard ActivityStreams fields.
*/
public static List extendedContext() {
Map extensions = new LinkedHashMap<>();
@@ -84,19 +64,6 @@ public final class ActivityPubContexts {
extensions.put("canQuote", typedRef("gts:canQuote"));
extensions.put("automaticApproval", typedRef("gts:automaticApproval"));
extensions.put("manualApproval", typedRef("gts:manualApproval"));
- extensions.put("fitpub", "https://fitpub.social/ns#");
- extensions.put("workoutData", "fitpub:workoutData");
- extensions.put("activityType", "fitpub:activityType");
- extensions.put("description", "fitpub:description");
- extensions.put("distance", "fitpub:distance");
- extensions.put("duration", "fitpub:duration");
- extensions.put("elevationGain", "fitpub:elevationGain");
- extensions.put("averagePace", "fitpub:averagePace");
- extensions.put("averageHeartRate", "fitpub:averageHeartRate");
- extensions.put("averageSpeed", "fitpub:averageSpeed");
- extensions.put("maxSpeed", "fitpub:maxSpeed");
- extensions.put("calories", "fitpub:calories");
- extensions.put("route", "fitpub:route");
return List.of(
"https://www.w3.org/ns/activitystreams",
extensions
diff --git a/src/main/resources/db/migration/V32__add_simplified_track_to_remote_activities.sql b/src/main/resources/db/migration/V32__add_simplified_track_to_remote_activities.sql
deleted file mode 100644
index 49e3b7e..0000000
--- a/src/main/resources/db/migration/V32__add_simplified_track_to_remote_activities.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-ALTER TABLE remote_activities
- ADD COLUMN simplified_track geometry(LineString, 4326);
-
-CREATE INDEX idx_remote_activity_simplified_track
- ON remote_activities
- USING gist (simplified_track);
-
-COMMENT ON COLUMN remote_activities.simplified_track IS
- 'Simplified remote route geometry for local map rendering';
diff --git a/src/main/resources/templates/profile/public.html b/src/main/resources/templates/profile/public.html
index 828877d..ef43a1b 100644
--- a/src/main/resources/templates/profile/public.html
+++ b/src/main/resources/templates/profile/public.html
@@ -46,7 +46,7 @@
-
+
diff --git a/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java b/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
index 4819da6..3053571 100644
--- a/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
+++ b/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
@@ -4,6 +4,7 @@ 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.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.utility.DockerImageName;
/**
@@ -22,6 +23,8 @@ public class TestcontainersConfiguration {
)
.withDatabaseName("testdb")
.withUsername("test")
- .withPassword("test");
+ .withPassword("test")
+ .waitingFor(new HostPortWaitStrategy())
+ .withReuse(true);
}
}
diff --git a/src/test/java/net/javahippie/fitpub/controller/ActivityPubControllerTest.java b/src/test/java/net/javahippie/fitpub/controller/ActivityPubControllerTest.java
deleted file mode 100644
index a0d9129..0000000
--- a/src/test/java/net/javahippie/fitpub/controller/ActivityPubControllerTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package net.javahippie.fitpub.controller;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import net.javahippie.fitpub.model.entity.Activity;
-import net.javahippie.fitpub.model.entity.User;
-import net.javahippie.fitpub.repository.ActivityRepository;
-import net.javahippie.fitpub.repository.FollowRepository;
-import net.javahippie.fitpub.repository.UserRepository;
-import net.javahippie.fitpub.security.HttpSignatureValidator;
-import net.javahippie.fitpub.service.ActivityImageService;
-import net.javahippie.fitpub.service.FederationService;
-import net.javahippie.fitpub.service.InboxProcessor;
-import net.javahippie.fitpub.service.WorkoutDataPayloadBuilder;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.http.ResponseEntity;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.io.File;
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-@DisplayName("ActivityPubController Tests")
-class ActivityPubControllerTest {
-
- @Mock
- private UserRepository userRepository;
-
- @Mock
- private ActivityRepository activityRepository;
-
- @Mock
- private ActivityImageService activityImageService;
-
- @Mock
- private InboxProcessor inboxProcessor;
-
- @Mock
- private FollowRepository followRepository;
-
- @Mock
- private HttpSignatureValidator signatureValidator;
-
- @Mock
- private FederationService federationService;
-
- @Mock
- private ObjectMapper objectMapper;
-
- @Mock
- private WorkoutDataPayloadBuilder workoutDataPayloadBuilder;
-
- @InjectMocks
- private ActivityPubController controller;
-
- private UUID activityId;
- private UUID userId;
- private Activity activity;
- private User user;
- private LocalDateTime createdAt;
-
- @BeforeEach
- void setUp() {
- activityId = UUID.randomUUID();
- userId = UUID.randomUUID();
- createdAt = LocalDateTime.of(2026, 5, 2, 9, 24, 50, 921_241_000);
-
- ReflectionTestUtils.setField(controller, "baseUrl", "https://fitpub.example");
-
- activity = Activity.builder()
- .id(activityId)
- .userId(userId)
- .activityType(Activity.ActivityType.RUN)
- .title("Lunch Run")
- .description("Sunny run")
- .visibility(Activity.Visibility.PUBLIC)
- .totalDistance(BigDecimal.valueOf(5000))
- .totalDurationSeconds(1800L)
- .createdAt(createdAt)
- .build();
-
- user = User.builder()
- .id(userId)
- .username("JaneDoe")
- .email("janedoe@example.com")
- .publicKey("public-key")
- .privateKey("private-key")
- .build();
- }
-
- @Test
- @DisplayName("Should serialize activity published timestamp with timezone")
- void getActivity_ShouldSerializePublishedTimestampWithTimezone() {
- when(activityRepository.findById(activityId)).thenReturn(Optional.of(activity));
- when(userRepository.findById(userId)).thenReturn(Optional.of(user));
- when(activityImageService.getActivityImageFile(activityId)).thenReturn(new File("/definitely/nonexistent-fitpub-test-image"));
-
- ResponseEntity> response = controller.getActivity(activityId);
-
- assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
- assertThat(response.getBody()).isNotNull();
- assertThat(response.getBody().get("published"))
- .isEqualTo(createdAt.atOffset(ZoneOffset.UTC).toInstant().toString());
- }
-
- @Test
- @DisplayName("Should include workoutData and FitPub context terms in activity note")
- void getActivity_ShouldIncludeWorkoutDataAndExtendedContext() {
- when(activityRepository.findById(activityId)).thenReturn(Optional.of(activity));
- when(userRepository.findById(userId)).thenReturn(Optional.of(user));
- when(activityImageService.getActivityImageFile(activityId)).thenReturn(new File("/definitely/nonexistent-fitpub-test-image"));
- when(workoutDataPayloadBuilder.build(activity)).thenReturn(Map.of(
- "activityType", "RUN",
- "description", "Sunny run",
- "distance", 5000L,
- "duration", "PT30M",
- "averagePace", "PT6M",
- "route", Map.of(
- "type", "FeatureCollection",
- "features", List.of()
- )
- ));
-
- ResponseEntity> response = controller.getActivity(activityId);
-
- assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
- assertThat(response.getBody()).isNotNull();
- assertThat(response.getBody().get("workoutData")).isEqualTo(Map.of(
- "activityType", "RUN",
- "description", "Sunny run",
- "distance", 5000L,
- "duration", "PT30M",
- "averagePace", "PT6M",
- "route", Map.of(
- "type", "FeatureCollection",
- "features", List.of()
- )
- ));
-
- @SuppressWarnings("unchecked")
- List context = (List) response.getBody().get("@context");
- assertThat(context).hasSize(2);
-
- @SuppressWarnings("unchecked")
- Map extensions = (Map) context.get(1);
- assertThat(extensions)
- .containsEntry("fitpub", "https://fitpub.social/ns#")
- .containsEntry("workoutData", "fitpub:workoutData")
- .containsEntry("route", "fitpub:route");
- }
-}
diff --git a/src/test/java/net/javahippie/fitpub/integration/FederationFollowFlowIntegrationTest.java b/src/test/java/net/javahippie/fitpub/integration/FederationFollowFlowIntegrationTest.java
index b07d325..99e3411 100644
--- a/src/test/java/net/javahippie/fitpub/integration/FederationFollowFlowIntegrationTest.java
+++ b/src/test/java/net/javahippie/fitpub/integration/FederationFollowFlowIntegrationTest.java
@@ -2,25 +2,19 @@ package net.javahippie.fitpub.integration;
import com.fasterxml.jackson.databind.ObjectMapper;
import net.javahippie.fitpub.config.TestcontainersConfiguration;
-import net.javahippie.fitpub.model.entity.Activity;
-import net.javahippie.fitpub.service.ActivityImageService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import net.javahippie.fitpub.model.entity.Follow;
import net.javahippie.fitpub.model.entity.RemoteActor;
-import net.javahippie.fitpub.model.entity.RemoteActivity;
import net.javahippie.fitpub.model.entity.User;
-import net.javahippie.fitpub.repository.ActivityRepository;
import net.javahippie.fitpub.repository.FollowRepository;
-import net.javahippie.fitpub.repository.RemoteActivityRepository;
import net.javahippie.fitpub.repository.RemoteActorRepository;
import net.javahippie.fitpub.repository.UserRepository;
import net.javahippie.fitpub.security.HttpSignatureValidator;
import net.javahippie.fitpub.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@@ -32,21 +26,15 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.transaction.annotation.Transactional;
-import java.io.File;
-import java.math.BigDecimal;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
-import java.time.LocalDateTime;
import java.util.Base64;
-import java.util.List;
import java.util.Map;
-import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@@ -75,12 +63,6 @@ class FederationFollowFlowIntegrationTest {
@Autowired
private RemoteActorRepository remoteActorRepository;
- @Autowired
- private RemoteActivityRepository remoteActivityRepository;
-
- @Autowired
- private ActivityRepository activityRepository;
-
@Autowired
private PasswordEncoder passwordEncoder;
@@ -90,9 +72,6 @@ class FederationFollowFlowIntegrationTest {
@Autowired
private HttpSignatureValidator signatureValidator;
- @MockBean
- private ActivityImageService activityImageService;
-
@Value("${fitpub.base-url}")
private String baseUrl;
@@ -122,22 +101,6 @@ class FederationFollowFlowIntegrationTest {
authToken = jwtTokenProvider.createToken(testUser.getUsername());
}
- private User createFederatedUser(String username, String email, String displayName) throws NoSuchAlgorithmException {
- KeyPair keyPair = generateRsaKeyPair();
- String publicKey = encodePublicKey(keyPair.getPublic().getEncoded());
- String privateKey = encodePrivateKey(keyPair.getPrivate().getEncoded());
-
- return userRepository.save(User.builder()
- .username(username)
- .email(email)
- .passwordHash(passwordEncoder.encode("password123"))
- .displayName(displayName)
- .publicKey(publicKey)
- .privateKey(privateKey)
- .enabled(true)
- .build());
- }
-
private KeyPair generateRsaKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048);
@@ -307,111 +270,6 @@ class FederationFollowFlowIntegrationTest {
assertThat(updatedFollow.getStatus()).isEqualTo(Follow.FollowStatus.ACCEPTED);
}
- @Test
- @DisplayName("Should import its own exported public activity through inbox")
- void testActivityRoundtripThroughExportAndInbox() throws Exception {
- User importingUser = testUser;
- User exportingUser = createFederatedUser("janedoe", "janedoe@example.com", "Jane Doe");
-
- Activity activity = activityRepository.save(Activity.builder()
- .userId(exportingUser.getId())
- .activityType(Activity.ActivityType.RUN)
- .title("Lunch Run")
- .description("Sunny run in the city")
- .startedAt(LocalDateTime.of(2026, 5, 2, 12, 0))
- .endedAt(LocalDateTime.of(2026, 5, 2, 12, 30))
- .createdAt(LocalDateTime.of(2026, 5, 2, 12, 31, 45, 123_000_000))
- .visibility(Activity.Visibility.PUBLIC)
- .totalDistance(BigDecimal.valueOf(5000))
- .totalDurationSeconds(1800L)
- .elevationGain(BigDecimal.valueOf(100))
- .sourceFileFormat("FIT")
- .published(true)
- .build());
-
- String exportingActorUri = baseUrl + "/users/" + exportingUser.getUsername();
- when(activityImageService.getActivityImageFile(activity.getId()))
- .thenReturn(new File("/definitely/nonexistent-fitpub-roundtrip-image"));
-
- remoteActorRepository.save(RemoteActor.builder()
- .actorUri(exportingActorUri)
- .username(exportingUser.getUsername())
- .domain(java.net.URI.create(baseUrl).getHost())
- .displayName(exportingUser.getDisplayName())
- .inboxUrl(exportingActorUri + "/inbox")
- .outboxUrl(exportingActorUri + "/outbox")
- .publicKey(exportingUser.getPublicKey())
- .publicKeyId(exportingActorUri + "#main-key")
- .lastFetchedAt(Instant.now())
- .build());
-
- followRepository.save(Follow.builder()
- .followerId(importingUser.getId())
- .followingActorUri(exportingActorUri)
- .status(Follow.FollowStatus.ACCEPTED)
- .activityId(baseUrl + "/activities/follow/" + UUID.randomUUID())
- .build());
-
- MvcResult exportResult = mockMvc.perform(get("/activities/" + activity.getId())
- .accept("application/activity+json"))
- .andExpect(status().isOk())
- .andReturn();
-
- @SuppressWarnings("unchecked")
- Map exportedNote = objectMapper.readValue(exportResult.getResponse().getContentAsByteArray(), Map.class);
-
- Map createActivity = Map.of(
- "@context", "https://www.w3.org/ns/activitystreams",
- "type", "Create",
- "id", baseUrl + "/activities/create/" + UUID.randomUUID(),
- "actor", exportingActorUri,
- "object", exportedNote
- );
-
- String privateKeyPem = exportingUser.getPrivateKey();
- String inboxPath = "/users/" + importingUser.getUsername() + "/inbox";
- String inboxUrl = "http://localhost" + inboxPath;
- String body = objectMapper.writeValueAsString(createActivity);
- HttpSignatureValidator.SignatureHeaders sigHeaders = signatureValidator.signRequest(
- "POST", inboxUrl, body, privateKeyPem, exportingActorUri + "#main-key"
- );
-
- mockMvc.perform(post(inboxPath)
- .contentType("application/activity+json")
- .header("Host", sigHeaders.host)
- .header("Date", sigHeaders.date)
- .header("Digest", sigHeaders.digest)
- .header("Signature", sigHeaders.signature)
- .content(body))
- .andExpect(status().isAccepted());
-
- RemoteActivity imported = remoteActivityRepository.findByActivityUri((String) exportedNote.get("id"))
- .orElseThrow();
-
- @SuppressWarnings("unchecked")
- Map workoutData = (Map) exportedNote.get("workoutData");
-
- assertThat(imported.getActivityUri()).isEqualTo(exportedNote.get("id"));
- assertThat(imported.getRemoteActorUri()).isEqualTo(exportingActorUri);
- assertThat(imported.getTitle()).isEqualTo(exportedNote.getOrDefault("name",
- exportedNote.getOrDefault("summary", "Untitled Activity")));
- assertThat(imported.getDescription()).isEqualTo(workoutData.get("description"));
- assertThat(imported.getPublishedAt()).isEqualTo(Instant.parse((String) exportedNote.get("published")));
- assertThat(imported.getVisibility()).isEqualTo(RemoteActivity.Visibility.PUBLIC);
- assertThat(imported.getActivityType()).isEqualTo(workoutData.get("activityType"));
- assertThat(imported.getTotalDistance()).isEqualTo(5000L);
- assertThat(imported.getTotalDurationSeconds()).isEqualTo(1800L);
- assertThat(imported.getElevationGain()).isEqualTo(workoutData.get("elevationGain"));
- assertThat(imported.getAveragePaceSeconds()).isNull();
- assertThat(imported.getAverageHeartRate()).isNull();
- assertThat(imported.getMaxSpeed()).isNull();
- assertThat(imported.getAverageSpeed()).isNull();
- assertThat(imported.getCalories()).isNull();
- assertThat(imported.getMapImageUrl()).isNull();
- assertThat(imported.getTrackGeojsonUrl()).isNull();
- assertThat(imported.getSimplifiedTrack()).isNull();
- }
-
@Test
@DisplayName("Should reject inbox POST without HTTP signature with 401")
void testInboxRejectsUnsignedRequest() throws Exception {
@@ -452,23 +310,6 @@ class FederationFollowFlowIntegrationTest {
.andExpect(status().isUnauthorized());
}
- private String stripHtml(String html) {
- if (html == null) {
- return "";
- }
- return html
- .replaceAll(" ", "\n")
- .replaceAll("", "")
- .replaceAll("
", "\n")
- .replaceAll("<[^>]+>", "")
- .replace("<", "<")
- .replace(">", ">")
- .replace(""", "\"")
- .replace("'", "'")
- .replace("&", "&")
- .trim();
- }
-
@Test
@DisplayName("Should process Undo Follow activity and remove follow relationship")
void testProcessUndoFollowActivity() throws Exception {
diff --git a/src/test/java/net/javahippie/fitpub/service/ActivityImageServiceTest.java b/src/test/java/net/javahippie/fitpub/service/ActivityImageServiceTest.java
index 0343ab4..687eb45 100644
--- a/src/test/java/net/javahippie/fitpub/service/ActivityImageServiceTest.java
+++ b/src/test/java/net/javahippie/fitpub/service/ActivityImageServiceTest.java
@@ -27,16 +27,12 @@ import static org.junit.jupiter.api.Assertions.*;
/**
* Manual test for ActivityImageService.
* These tests are disabled by default and should only be run manually.
- *
- * To run this test manually:
- * mvn test -Dtest=ActivityImageServiceTest
*/
@SpringBootTest(properties = {
"fitpub.image.osm-tiles.enabled=true"
})
@ActiveProfiles("test")
@Import(TestcontainersConfiguration.class)
-@Disabled("Manual test - run explicitly when needed")
class ActivityImageServiceTest {
@Autowired
@@ -59,6 +55,7 @@ class ActivityImageServiceTest {
* mvn test -Dtest=ActivityImageServiceTest#testGenerateActivityImage_Manual
*/
@Test
+ @Disabled("Manual test - run explicitly when needed")
@DisplayName("Generate activity image from test FIT file")
void testGenerateActivityImage_Manual() throws Exception {
// Load test FIT file
diff --git a/src/test/java/net/javahippie/fitpub/service/ActivityPostProcessingServiceTest.java b/src/test/java/net/javahippie/fitpub/service/ActivityPostProcessingServiceTest.java
index 5507c23..08ef492 100644
--- a/src/test/java/net/javahippie/fitpub/service/ActivityPostProcessingServiceTest.java
+++ b/src/test/java/net/javahippie/fitpub/service/ActivityPostProcessingServiceTest.java
@@ -1,42 +1,25 @@
package net.javahippie.fitpub.service;
-import net.javahippie.fitpub.model.entity.Activity;
-import net.javahippie.fitpub.model.entity.ActivityMetrics;
-import net.javahippie.fitpub.model.entity.User;
-import net.javahippie.fitpub.repository.ActivityRepository;
-import net.javahippie.fitpub.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import net.javahippie.fitpub.model.entity.Activity;
+import net.javahippie.fitpub.model.entity.User;
+import net.javahippie.fitpub.repository.ActivityRepository;
+import net.javahippie.fitpub.repository.UserRepository;
import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import java.util.Optional;
import java.util.UUID;
-import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.anyBoolean;
-import static org.mockito.Mockito.anyString;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.eq;
-import static org.mockito.Mockito.lenient;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
/**
* Unit tests for ActivityPostProcessingService.
@@ -66,9 +49,6 @@ class ActivityPostProcessingServiceTest {
@Mock
private UserRepository userRepository;
- @Mock
- private WorkoutDataPayloadBuilder workoutDataPayloadBuilder;
-
@InjectMocks
private ActivityPostProcessingService service;
@@ -76,13 +56,11 @@ class ActivityPostProcessingServiceTest {
private UUID userId;
private Activity testActivity;
private User testUser;
- private LocalDateTime createdAt;
@BeforeEach
void setUp() {
activityId = UUID.randomUUID();
userId = UUID.randomUUID();
- createdAt = LocalDateTime.of(2026, 5, 2, 9, 24, 50, 921_241_000);
// Set baseUrl via reflection (since it's @Value injected)
ReflectionTestUtils.setField(service, "baseUrl", "https://test.example");
@@ -98,39 +76,9 @@ class ActivityPostProcessingServiceTest {
.totalDistance(BigDecimal.valueOf(5000))
.totalDurationSeconds(1800L)
.elevationGain(BigDecimal.valueOf(100))
- .startedAt(createdAt.minusMinutes(30))
- .createdAt(createdAt)
- .simplifiedTrack(new GeometryFactory().createLineString(new Coordinate[]{
- new Coordinate(8.55, 47.37),
- new Coordinate(8.56, 47.38)
- }))
+ .startedAt(LocalDateTime.now())
+ .createdAt(LocalDateTime.now())
.build();
- testActivity.setMetrics(ActivityMetrics.builder()
- .averagePaceSeconds(321L)
- .build());
- Map workoutData = new HashMap<>();
- workoutData.put("activityType", "RUN");
- workoutData.put("description", "Morning jog");
- workoutData.put("distance", 5000L);
- workoutData.put("duration", "PT30M");
- workoutData.put("averagePace", "PT5M21S");
- workoutData.put("elevationGain", 100);
- workoutData.put("route", Map.of(
- "type", "FeatureCollection",
- "features", List.of(
- Map.of(
- "type", "Feature",
- "geometry", Map.of(
- "type", "LineString",
- "coordinates", List.of(
- List.of(8.55, 47.37),
- List.of(8.56, 47.38)
- )
- )
- )
- )
- ));
- lenient().when(workoutDataPayloadBuilder.build(testActivity)).thenReturn(workoutData);
// Create test user
testUser = User.builder()
@@ -284,24 +232,6 @@ class ActivityPostProcessingServiceTest {
verify(federationService).sendCreateActivity(anyString(), any(), eq(testUser), eq(false));
}
- @Test
- @DisplayName("Should serialize federation note published timestamp with timezone")
- void testPublishToFederationAsync_PublishedTimestampIncludesTimezone() {
- when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
- when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
- when(activityImageService.generateActivityImage(testActivity)).thenReturn(null);
- doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
-
- @SuppressWarnings("unchecked")
- ArgumentCaptor> noteCaptor = ArgumentCaptor.forClass(java.util.Map.class);
-
- service.publishToFederationAsync(activityId, userId);
-
- verify(federationService).sendCreateActivity(anyString(), noteCaptor.capture(), eq(testUser), eq(true));
- assertThat(noteCaptor.getValue().get("published"))
- .isEqualTo(createdAt.atOffset(ZoneOffset.UTC).toInstant().toString());
- }
-
@Test
@DisplayName("Should skip federation for PRIVATE activity")
void testPublishToFederationAsync_PrivateActivity() {
@@ -387,47 +317,4 @@ class ActivityPostProcessingServiceTest {
// Then: Verify federation was called (content formatting is tested indirectly)
verify(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
}
-
- @Test
- @DisplayName("Should include workoutData payload in federation note")
- void testPublishToFederationAsync_IncludesWorkoutDataPayload() {
- when(activityRepository.findById(activityId)).thenReturn(Optional.of(testActivity));
- when(userRepository.findById(userId)).thenReturn(Optional.of(testUser));
- when(activityImageService.generateActivityImage(testActivity)).thenReturn(null);
- doNothing().when(federationService).sendCreateActivity(anyString(), any(), any(), anyBoolean());
-
- @SuppressWarnings("unchecked")
- ArgumentCaptor> noteCaptor = ArgumentCaptor.forClass(Map.class);
-
- service.publishToFederationAsync(activityId, userId);
-
- verify(federationService).sendCreateActivity(anyString(), noteCaptor.capture(), eq(testUser), eq(true));
-
- @SuppressWarnings("unchecked")
- Map workoutData = (Map) noteCaptor.getValue().get("workoutData");
- assertThat(workoutData)
- .containsEntry("activityType", "RUN")
- .containsEntry("description", "Morning jog")
- .containsEntry("distance", 5000L)
- .containsEntry("duration", "PT30M")
- .containsEntry("averagePace", "PT5M21S")
- .containsEntry("elevationGain", 100);
-
- @SuppressWarnings("unchecked")
- Map route = (Map) workoutData.get("route");
- assertThat(route).containsEntry("type", "FeatureCollection");
-
- @SuppressWarnings("unchecked")
- List> features = (List>) route.get("features");
- assertThat(features).hasSize(1);
- assertThat(features.get(0)).containsEntry("type", "Feature");
-
- @SuppressWarnings("unchecked")
- Map geometry = (Map) features.get(0).get("geometry");
- assertThat(geometry).containsEntry("type", "LineString");
- assertThat(geometry.get("coordinates")).isEqualTo(List.of(
- List.of(8.55, 47.37),
- List.of(8.56, 47.38)
- ));
- }
}
diff --git a/src/test/java/net/javahippie/fitpub/service/InboxProcessorTest.java b/src/test/java/net/javahippie/fitpub/service/InboxProcessorTest.java
deleted file mode 100644
index f1ae088..0000000
--- a/src/test/java/net/javahippie/fitpub/service/InboxProcessorTest.java
+++ /dev/null
@@ -1,217 +0,0 @@
-package net.javahippie.fitpub.service;
-
-import net.javahippie.fitpub.model.entity.Follow;
-import net.javahippie.fitpub.model.entity.RemoteActivity;
-import net.javahippie.fitpub.model.entity.RemoteActor;
-import net.javahippie.fitpub.model.entity.User;
-import net.javahippie.fitpub.repository.ActivityRepository;
-import net.javahippie.fitpub.repository.CommentRepository;
-import net.javahippie.fitpub.repository.FollowRepository;
-import net.javahippie.fitpub.repository.LikeRepository;
-import net.javahippie.fitpub.repository.RemoteActivityRepository;
-import net.javahippie.fitpub.repository.RemoteActorRepository;
-import net.javahippie.fitpub.repository.UserRepository;
-import org.locationtech.jts.geom.LineString;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.test.util.ReflectionTestUtils;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-@DisplayName("InboxProcessor Tests")
-class InboxProcessorTest {
-
- @Mock
- private UserRepository userRepository;
-
- @Mock
- private FollowRepository followRepository;
-
- @Mock
- private FederationService federationService;
-
- @Mock
- private ActivityRepository activityRepository;
-
- @Mock
- private LikeRepository likeRepository;
-
- @Mock
- private CommentRepository commentRepository;
-
- @Mock
- private NotificationService notificationService;
-
- @Mock
- private RemoteActivityRepository remoteActivityRepository;
-
- @Mock
- private RemoteActorRepository remoteActorRepository;
-
- @InjectMocks
- private InboxProcessor inboxProcessor;
-
- private User localUser;
- private String remoteActorUri;
-
- @BeforeEach
- void setUp() {
- localUser = User.builder()
- .id(UUID.randomUUID())
- .username("JaneDoe")
- .email("janedoe@example.com")
- .passwordHash("irrelevant")
- .publicKey("public-key")
- .privateKey("private-key")
- .build();
-
- remoteActorUri = "https://fitpub.example.com/users/JohnDoe";
-
- ReflectionTestUtils.setField(inboxProcessor, "baseUrl", "https://fitpub.example");
- }
-
- @Test
- @DisplayName("Should persist remote activity when published timestamp has fractional seconds but no timezone")
- void processCreateRemoteActivity_WithPublishedTimestampWithoutTimezone_ShouldPersistRemoteActivity() {
- when(remoteActivityRepository.existsByActivityUri("https://fitpub.example.com/activities/123"))
- .thenReturn(false);
- when(federationService.fetchRemoteActor(remoteActorUri)).thenReturn(RemoteActor.builder()
- .actorUri(remoteActorUri)
- .username("JohnDoe")
- .domain("fitpub.example.com")
- .inboxUrl("https://fitpub.example.com/users/JohnDoe/inbox")
- .publicKey("public-key")
- .build());
- when(userRepository.findByUsername("JaneDoe")).thenReturn(Optional.of(localUser));
- when(followRepository.findByFollowerIdAndFollowingActorUri(localUser.getId(), remoteActorUri))
- .thenReturn(Optional.of(Follow.builder()
- .followerId(localUser.getId())
- .followingActorUri(remoteActorUri)
- .status(Follow.FollowStatus.ACCEPTED)
- .build()));
-
- Map note = Map.of(
- "id", "https://fitpub.example.com/activities/123",
- "type", "Note",
- "name", "Lunch Run",
- "content", "Sunny run
",
- "published", "2026-05-02T09:24:50.921241",
- "to", List.of("https://www.w3.org/ns/activitystreams#Public")
- );
-
- Map activity = Map.of(
- "type", "Create",
- "actor", remoteActorUri,
- "object", note
- );
-
- ArgumentCaptor remoteActivityCaptor =
- ArgumentCaptor.forClass(net.javahippie.fitpub.model.entity.RemoteActivity.class);
-
- inboxProcessor.processActivity("JaneDoe", activity);
-
- verify(remoteActivityRepository).existsByActivityUri("https://fitpub.example.com/activities/123");
- verify(federationService).fetchRemoteActor(remoteActorUri);
- verify(remoteActivityRepository).save(remoteActivityCaptor.capture());
-
- assertThat(remoteActivityCaptor.getValue().getPublishedAt())
- .isEqualTo(Instant.parse("2026-05-02T09:24:50.921241Z"));
- }
-
- @Test
- @DisplayName("Should prefer workoutData fields over legacy content parsing")
- void processCreateRemoteActivity_WithWorkoutDataPayload_ShouldPreferWorkoutDataFields() {
- when(remoteActivityRepository.existsByActivityUri("https://fitpub.example.com/activities/456"))
- .thenReturn(false);
- when(federationService.fetchRemoteActor(remoteActorUri)).thenReturn(RemoteActor.builder()
- .actorUri(remoteActorUri)
- .username("JohnDoe")
- .domain("fitpub.example.com")
- .inboxUrl("https://fitpub.example.com/users/JohnDoe/inbox")
- .publicKey("public-key")
- .build());
- when(userRepository.findByUsername("JaneDoe")).thenReturn(Optional.of(localUser));
- when(followRepository.findByFollowerIdAndFollowingActorUri(localUser.getId(), remoteActorUri))
- .thenReturn(Optional.of(Follow.builder()
- .followerId(localUser.getId())
- .followingActorUri(remoteActorUri)
- .status(Follow.FollowStatus.ACCEPTED)
- .build()));
-
- Map workoutData = new HashMap<>();
- workoutData.put("activityType", "RUN");
- workoutData.put("description", "Direct workoutData description");
- workoutData.put("distance", 9800L);
- workoutData.put("duration", "PT41M9S");
- workoutData.put("averagePace", "PT4M12S");
- workoutData.put("elevationGain", 123);
- workoutData.put("route", Map.of(
- "type", "FeatureCollection",
- "features", List.of(Map.of(
- "type", "Feature",
- "geometry", Map.of(
- "type", "LineString",
- "coordinates", List.of(
- List.of(8.55, 47.37),
- List.of(8.56, 47.38),
- List.of(8.57, 47.39)
- )
- )
- ))
- ));
-
- Map note = Map.of(
- "id", "https://fitpub.example.com/activities/456",
- "type", "Note",
- "name", "Kraremanns Lauf 2026",
- "content", "Kraremanns Lauf 2026
Run · 9.80 km · 41:09
Legacy content fallback
",
- "published", "2026-05-02T09:24:50.921241",
- "to", List.of("https://www.w3.org/ns/activitystreams#Public"),
- "workoutData", workoutData
- );
-
- Map activity = Map.of(
- "type", "Create",
- "actor", remoteActorUri,
- "object", note
- );
-
- ArgumentCaptor remoteActivityCaptor =
- ArgumentCaptor.forClass(RemoteActivity.class);
-
- inboxProcessor.processActivity("JaneDoe", activity);
-
- verify(remoteActivityRepository).save(remoteActivityCaptor.capture());
-
- RemoteActivity remoteActivity = remoteActivityCaptor.getValue();
- assertThat(remoteActivity.getTitle()).isEqualTo("Kraremanns Lauf 2026");
- assertThat(remoteActivity.getDescription()).isEqualTo("Direct workoutData description");
- assertThat(remoteActivity.getActivityType()).isEqualTo("RUN");
- assertThat(remoteActivity.getTotalDistance()).isEqualTo(9800L);
- assertThat(remoteActivity.getTotalDurationSeconds()).isEqualTo(2469L);
- assertThat(remoteActivity.getAveragePaceSeconds()).isEqualTo(252L);
- assertThat(remoteActivity.getElevationGain()).isEqualTo(123);
- LineString simplifiedTrack = remoteActivity.getSimplifiedTrack();
- assertThat(simplifiedTrack).isNotNull();
- assertThat(simplifiedTrack.getNumPoints()).isEqualTo(3);
- assertThat(simplifiedTrack.getSRID()).isEqualTo(4326);
- assertThat(simplifiedTrack.getCoordinateN(0).x).isEqualTo(8.55);
- assertThat(simplifiedTrack.getCoordinateN(0).y).isEqualTo(47.37);
- }
-}
diff --git a/src/test/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilderTest.java b/src/test/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilderTest.java
deleted file mode 100644
index bc21615..0000000
--- a/src/test/java/net/javahippie/fitpub/service/WorkoutDataPayloadBuilderTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package net.javahippie.fitpub.service;
-
-import net.javahippie.fitpub.model.entity.Activity;
-import net.javahippie.fitpub.model.entity.ActivityMetrics;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.math.BigDecimal;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-@DisplayName("WorkoutDataPayloadBuilder Tests")
-class WorkoutDataPayloadBuilderTest {
-
- @Mock
- private PrivacyZoneService privacyZoneService;
-
- @Mock
- private TrackPrivacyFilter trackPrivacyFilter;
-
- @InjectMocks
- private WorkoutDataPayloadBuilder builder;
-
- private UUID userId;
- private Activity activity;
-
- @BeforeEach
- void setUp() {
- userId = UUID.randomUUID();
- activity = Activity.builder()
- .id(UUID.randomUUID())
- .userId(userId)
- .activityType(Activity.ActivityType.RUN)
- .description("Morning jog")
- .visibility(Activity.Visibility.PUBLIC)
- .totalDistance(BigDecimal.valueOf(5000))
- .totalDurationSeconds(1800L)
- .elevationGain(BigDecimal.valueOf(100))
- .simplifiedTrack(new GeometryFactory().createLineString(new Coordinate[]{
- new Coordinate(8.55, 47.37),
- new Coordinate(8.56, 47.38)
- }))
- .build();
- activity.setMetrics(ActivityMetrics.builder()
- .averagePaceSeconds(321L)
- .averageHeartRate(150)
- .averageSpeed(BigDecimal.valueOf(10.4))
- .maxSpeed(BigDecimal.valueOf(14.2))
- .calories(420)
- .build());
- }
-
- @Test
- @DisplayName("Should build workoutData payload with route and metrics")
- void build_ShouldIncludeWorkoutDataRouteAndMetrics() {
- when(privacyZoneService.getActivePrivacyZones(userId)).thenReturn(List.of());
-
- Map workoutData = builder.build(activity);
-
- assertThat(workoutData)
- .containsEntry("activityType", "RUN")
- .containsEntry("description", "Morning jog")
- .containsEntry("distance", 5000L)
- .containsEntry("duration", "PT30M")
- .containsEntry("elevationGain", 100)
- .containsEntry("averagePace", "PT5M21S")
- .containsEntry("averageHeartRate", 150)
- .containsEntry("averageSpeed", 10.4)
- .containsEntry("maxSpeed", 14.2)
- .containsEntry("calories", 420);
-
- @SuppressWarnings("unchecked")
- Map route = (Map) workoutData.get("route");
- assertThat(route).containsEntry("type", "FeatureCollection");
-
- @SuppressWarnings("unchecked")
- List> features = (List>) route.get("features");
- assertThat(features).hasSize(1);
-
- @SuppressWarnings("unchecked")
- Map geometry = (Map) features.get(0).get("geometry");
- assertThat(geometry).containsEntry("type", "LineString");
- assertThat(geometry.get("coordinates")).isEqualTo(List.of(
- List.of(8.55, 47.37),
- List.of(8.56, 47.38)
- ));
- }
-}