diff --git a/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupTimeZoneTest.java b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupTimeZoneTest.java new file mode 100644 index 00000000000..6e195f23597 --- /dev/null +++ b/java/com.sap.sailing.server.gateway.test/src/com/sap/sailing/server/gateway/test/jaxrs/LeaderboardGroupTimeZoneTest.java @@ -0,0 +1,80 @@ +package com.sap.sailing.server.gateway.test.jaxrs; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Collections; +import java.util.TimeZone; +import java.util.UUID; + +import javax.ws.rs.core.Response; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.ResourceLock; + +import com.sap.sailing.domain.common.LeaderboardNameConstants; +import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.LeaderboardGroup; +import com.sap.sailing.domain.leaderboard.ScoringScheme; +import com.sap.sailing.domain.leaderboard.SettableScoreCorrection; +import com.sap.sailing.server.gateway.serialization.LeaderboardGroupConstants; +import com.sap.sailing.server.interfaces.RacingEventService; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.rest.StreamingOutputUtil; +import com.sap.sse.security.SecurityService; + +public class LeaderboardGroupTimeZoneTest { + @Test + @ResourceLock("java.util.TimeZone.default") + public void testLastScoringUpdateIsUtcRegardlessOfDefaultTimeZone() throws Exception { + final TimeZone originalTimeZone = TimeZone.getDefault(); + try { + // Start outside UTC so initialization cannot accidentally capture UTC as the default. + TimeZone.setDefault(TimeZone.getTimeZone("Europe/Berlin")); + final TimePoint lastUpdate = new MillisecondsTimePoint(Instant.parse("2026-10-09T12:13:04Z").toEpochMilli()); + final SettableScoreCorrection scoreCorrection = mock(SettableScoreCorrection.class); + when(scoreCorrection.getTimePointOfLastCorrectionsValidity()).thenReturn(lastUpdate); + final Leaderboard leaderboard = mock(Leaderboard.class); + when(leaderboard.getName()).thenReturn("CNR_2026_F36"); + when(leaderboard.getScoreCorrection()).thenReturn(scoreCorrection); + when(leaderboard.getScoringScheme()).thenReturn(mock(ScoringScheme.class)); + final LeaderboardGroup leaderboardGroup = mock(LeaderboardGroup.class); + when(leaderboardGroup.getName()).thenReturn("CNR_2026_GROUP"); + when(leaderboardGroup.getId()).thenReturn(UUID.fromString("12e275d9-37ef-441d-8376-221d2ef07e04")); + when(leaderboardGroup.getLeaderboards()).thenReturn(Collections.singletonList(leaderboard)); + final RacingEventService service = mock(RacingEventService.class); + when(service.getLeaderboardGroupByName(leaderboardGroup.getName())).thenReturn(leaderboardGroup); + when(service.getAllEvents()).thenReturn(Collections.emptyList()); + final SecurityService securityService = mock(SecurityService.class); + when(securityService.hasCurrentUserReadPermission(leaderboardGroup)).thenReturn(/* value */ true); + when(securityService.hasCurrentUserReadPermission(leaderboard)).thenReturn(/* value */ true); + final DummyLeaderboardGroupsResource resource = spy(new DummyLeaderboardGroupsResource()); + doReturn(service).when(resource).getService(); + doReturn(securityService).when(resource).getSecurityService(); + for (final String zoneId : new String[] { "Europe/Berlin", "UTC", "America/Los_Angeles", "UTC", + "Asia/Kolkata", "Europe/Berlin", "UTC" }) { + TimeZone.setDefault(TimeZone.getTimeZone(zoneId)); + final Response response = resource.getLeaderboardGroup(leaderboardGroup.getName()); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus(), zoneId); + final JSONObject json = (JSONObject) JSONValue.parseWithException( + StreamingOutputUtil.getEntityAsString(response.getEntity())); + final JSONObject jsonLeaderboard = (JSONObject) ((JSONArray) json.get( + LeaderboardGroupConstants.LEADERBOARDS)).get(/* index */ 0); + assertEquals("Fri Oct 09 12:13:04 UTC 2026", + jsonLeaderboard.get(LeaderboardNameConstants.LASTSCORINGUPDATE), zoneId); + assertEquals(lastUpdate.asMillis(), + jsonLeaderboard.get(LeaderboardNameConstants.LASTSCORINGUPDATE_MILLIS), zoneId); + } + } finally { + TimeZone.setDefault(originalTimeZone); + } + } +} diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java index e01f0f08951..a1fff47b56f 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardGroupsResource.java @@ -1,11 +1,14 @@ package com.sap.sailing.server.gateway.jaxrs.api; import java.io.Serializable; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -61,6 +64,9 @@ public class LeaderboardGroupsResource extends AbstractSailingServerResource { protected static final String EXISTING_GROUP_ID_QUERY_PARAM = "existingGroupId"; protected static final String CREATE_NEW_GROUP_QUERY_PARAM = "createNewGroup"; protected static final String NAME_PATH_PARAM = "name"; + // Preserve the Date.toString() format, using UTC independently of the runtime's time zone and locale. + private static final DateTimeFormatter LAST_SCORING_UPDATE_FORMATTER = DateTimeFormatter + .ofPattern("EEE MMM dd HH:mm:ss zzz yyyy", Locale.ENGLISH).withZone(ZoneId.of("UTC")); @POST @Path("/{"+NAME_PATH_PARAM+"}/migrate") @@ -175,9 +181,11 @@ public class LeaderboardGroupsResource extends AbstractSailingServerResource { SettableScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); if (scoreCorrection != null) { jsonLeaderboard.put(LeaderboardNameConstants.SCORINGCOMMENT, scoreCorrection.getComment()); - TimePoint lastUpdateTimepoint = scoreCorrection.getTimePointOfLastCorrectionsValidity(); + final TimePoint lastUpdateTimepoint = scoreCorrection.getTimePointOfLastCorrectionsValidity(); jsonLeaderboard.put(LeaderboardNameConstants.LASTSCORINGUPDATE, - lastUpdateTimepoint != null ? lastUpdateTimepoint.asDate().toString() : null); + lastUpdateTimepoint != null + ? LAST_SCORING_UPDATE_FORMATTER.format(lastUpdateTimepoint.asDate().toInstant()) + : null); jsonLeaderboard.put(LeaderboardNameConstants.LASTSCORINGUPDATE_MILLIS, lastUpdateTimepoint != null ? lastUpdateTimepoint.asMillis() : null); } else {