fix(library): sync again after a Steam ID change and keep library on empty result

An empty Steam answer (error, private profile, wrong ID) was stored as OK and
soft-deleted the whole library. It is now FAILED, keeps the library and is
retried after 15 minutes (app.library.failed-retry-minutes).

The automatic sync now also runs when the Steam ID in the token differs from
the stored one, and removes the games of the previous Steam account.
This commit is contained in:
lobo committed 2026-09-24 08:56:45 +02:00
1 parent 8de138487e
commit 50f5f26157
4 files changed
+221 -6

No files matched your search

@@ -0,0 +1,104 @@
package core_service.modules.library.service;
import core_service.modules.library.domain.UserSteamSync;
import core_service.modules.library.repo.UserSteamSyncRepository;
import core_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LibrarySyncServiceTest extends AbstractUnitTest {
private static final long USER_ID = 1L;
private static final String STEAM_ID = "76561198000000001";
@Mock
private UserSteamSyncRepository userSteamSyncRepository;
@Mock
private LibrarySyncWorker worker;
@InjectMocks
private LibrarySyncService syncService;
@BeforeEach
void setUp() {
ReflectionTestUtils.setField(syncService, "syncTtlHours", 24L);
ReflectionTestUtils.setField(syncService, "forceSyncCooldownSeconds", 60L);
ReflectionTestUtils.setField(syncService, "runningStaleMinutes", 5L);
ReflectionTestUtils.setField(syncService, "failedRetryMinutes", 15L);
}
private void givenSync(String steamId, UserSteamSync.Status status, Duration age) {
UserSteamSync sync = new UserSteamSync(USER_ID, steamId);
sync.setLastSyncStatus(status);
sync.setLastSyncedAt(Instant.now().minus(age));
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(sync));
}
@Test
void syncIfStale_NoSyncYet_Runs() {
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.empty());
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker).runSync(USER_ID, STEAM_ID);
}
@Test
void syncIfStale_FreshOkSync_Skips() {
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofMinutes(10));
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker, never()).runSync(anyLong(), anyString());
}
@Test
void syncIfStale_FreshSyncForOtherSteamId_Runs() {
givenSync("76561198005679790", UserSteamSync.Status.OK, Duration.ofMinutes(10));
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker).runSync(USER_ID, STEAM_ID);
}
@Test
void syncIfStale_FailedSyncAfterRetryInterval_Runs() {
givenSync(STEAM_ID, UserSteamSync.Status.FAILED, Duration.ofMinutes(16));
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker).runSync(USER_ID, STEAM_ID);
}
@Test
void syncIfStale_FailedSyncWithinRetryInterval_Skips() {
givenSync(STEAM_ID, UserSteamSync.Status.FAILED, Duration.ofMinutes(5));
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker, never()).runSync(anyLong(), anyString());
}
@Test
void syncIfStale_RunningForOtherSteamId_Skips() {
givenSync("76561198005679790", UserSteamSync.Status.RUNNING, Duration.ofSeconds(10));
syncService.syncIfStale(USER_ID, STEAM_ID);
verify(worker, never()).runSync(anyLong(), anyString());
}
}
@@ -0,0 +1,92 @@
package core_service.modules.library.service;
import core_service.modules.library.domain.UserSteamSync;
import core_service.modules.library.repo.SteamGameRepository;
import core_service.modules.library.repo.UserGameRepository;
import core_service.modules.library.repo.UserSteamSyncRepository;
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
import core_service.modules.steam_adapter.service.SteamApiService;
import core_service.shared.test.AbstractUnitTest;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.List;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class LibrarySyncWorkerTest extends AbstractUnitTest {
private static final long USER_ID = 1L;
private static final String STEAM_ID = "76561198000000001";
@Mock
private SteamApiService steamApiService;
@Mock
private SteamGameRepository steamGameRepository;
@Mock
private UserGameRepository userGameRepository;
@Mock
private UserSteamSyncRepository userSteamSyncRepository;
@InjectMocks
private LibrarySyncWorker worker;
@Test
void runSync_EmptySteamResult_MarksFailedAndKeepsLibrary() {
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any())).thenReturn(1);
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of());
worker.runSync(USER_ID, STEAM_ID);
verify(userGameRepository, never()).softDeleteMissingSince(anyLong(), any(), any());
verify(userSteamSyncRepository).markFinished(eq(USER_ID), eq("FAILED"), any());
}
@Test
void runSync_EmptyResultForNewSteamId_RemovesOldLibrary() {
when(userSteamSyncRepository.findById(USER_ID))
.thenReturn(Optional.of(new UserSteamSync(USER_ID, "76561198005679790")));
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any())).thenReturn(1);
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of());
worker.runSync(USER_ID, STEAM_ID);
verify(userGameRepository).softDeleteMissingSince(eq(USER_ID), any(), any());
verify(userSteamSyncRepository).markFinished(eq(USER_ID), eq("FAILED"), any());
}
@Test
void runSync_SteamErrorForNewSteamId_RemovesOldLibrary() {
when(userSteamSyncRepository.findById(USER_ID))
.thenReturn(Optional.of(new UserSteamSync(USER_ID, "76561198005679790")));
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any())).thenReturn(1);
when(steamApiService.getOwnedGames(STEAM_ID)).thenThrow(new IllegalStateException("Steam down"));
worker.runSync(USER_ID, STEAM_ID);
verify(userGameRepository).softDeleteMissingSince(eq(USER_ID), any(), any());
verify(userSteamSyncRepository).markFinished(eq(USER_ID), eq("FAILED"), any());
}
@Test
void runSync_WithGames_MarksOk() {
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any())).thenReturn(1);
when(steamApiService.getOwnedGames(STEAM_ID)).thenReturn(List.of(
new SteamOwnedGamesResponse.Game(220, "Half-Life 2", 0, "icon", true)));
worker.runSync(USER_ID, STEAM_ID);
verify(userGameRepository).softDeleteMissingSince(eq(USER_ID), any(), any());
verify(userSteamSyncRepository).markFinished(eq(USER_ID), eq("OK"), any());
}
}