Public Access
feat(sync): start Steam sync on visit when data is older than 20 h
A visit starts a background sync when the last successful sync is older than 20 h; one try per hour, one sync at a time. Migration V31 adds last_success_at and login_sync_tried_at. The nightly sync skips only users with a successful sync today after 23:00. games-count returns syncStarted.
This commit is contained in:
1 parent
a8e208321f
commit
35b1db45e6
11 files changed
+446
-71
No files matched your search
@@ -115,10 +115,15 @@ public class LibraryController {
|
|||||||
@GetMapping("/games-count")
|
@GetMapping("/games-count")
|
||||||
public ResponseEntity<?> getGamesCount(@AuthenticationPrincipal UserPrincipal principal) {
|
public ResponseEntity<?> getGamesCount(@AuthenticationPrincipal UserPrincipal principal) {
|
||||||
Long userId = principal.getIdAsLong();
|
Long userId = principal.getIdAsLong();
|
||||||
principal.steamIdText().ifPresentOrElse(steamId -> syncService.syncIfStale(userId, steamId),
|
boolean syncStarted = principal.steamIdText()
|
||||||
() -> syncService.stopNightlySync(userId));
|
.map(steamId -> syncService.syncIfStale(userId, steamId))
|
||||||
|
.orElseGet(() -> {
|
||||||
|
syncService.stopNightlySync(userId);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
long count = queryService.countOwnedGames(userId);
|
long count = queryService.countOwnedGames(userId);
|
||||||
return ResponseEntity.ok(Map.of("count", count, "hasSteamId", principal.steamId() != null));
|
return ResponseEntity.ok(Map.of("count", count, "hasSteamId", principal.steamId() != null,
|
||||||
|
"syncStarted", syncStarted));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/games")
|
@GetMapping("/games")
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ package core_service.modules.library.domain;
|
|||||||
|
|
||||||
public enum SyncTrigger {
|
public enum SyncTrigger {
|
||||||
MANUAL,
|
MANUAL,
|
||||||
AUTO
|
AUTO,
|
||||||
|
LOGIN
|
||||||
}
|
}
|
||||||
@@ -45,6 +45,12 @@ public class UserSteamSync {
|
|||||||
@Column(name = "user_seen_at")
|
@Column(name = "user_seen_at")
|
||||||
private Instant userSeenAt;
|
private Instant userSeenAt;
|
||||||
|
|
||||||
|
@Column(name = "last_success_at")
|
||||||
|
private Instant lastSuccessAt;
|
||||||
|
|
||||||
|
@Column(name = "login_sync_tried_at")
|
||||||
|
private Instant loginSyncTriedAt;
|
||||||
|
|
||||||
public UserSteamSync(Long userId, String steamId) {
|
public UserSteamSync(Long userId, String steamId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.steamId = steamId;
|
this.steamId = steamId;
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ public interface UserSteamSyncRepository extends JpaRepository<UserSteamSync, Lo
|
|||||||
WHERE NOT EXISTS (SELECT 1 FROM deleted_user WHERE user_id = :userId)
|
WHERE NOT EXISTS (SELECT 1 FROM deleted_user WHERE user_id = :userId)
|
||||||
AND NOT EXISTS (SELECT 1 FROM blocked_user WHERE user_id = :userId)
|
AND NOT EXISTS (SELECT 1 FROM blocked_user WHERE user_id = :userId)
|
||||||
ON CONFLICT (user_id) DO UPDATE
|
ON CONFLICT (user_id) DO UPDATE
|
||||||
SET steam_id = EXCLUDED.steam_id,
|
SET last_success_at = CASE WHEN user_steam_sync.steam_id IS DISTINCT FROM EXCLUDED.steam_id
|
||||||
|
THEN NULL ELSE user_steam_sync.last_success_at END,
|
||||||
|
steam_id = EXCLUDED.steam_id,
|
||||||
last_sync_status = 'RUNNING',
|
last_sync_status = 'RUNNING',
|
||||||
sync_started_at = EXCLUDED.sync_started_at
|
sync_started_at = EXCLUDED.sync_started_at
|
||||||
WHERE user_steam_sync.last_sync_status <> 'RUNNING'
|
WHERE user_steam_sync.last_sync_status <> 'RUNNING'
|
||||||
@@ -50,6 +52,34 @@ public interface UserSteamSyncRepository extends JpaRepository<UserSteamSync, Lo
|
|||||||
@Param("startedAt") Instant startedAt,
|
@Param("startedAt") Instant startedAt,
|
||||||
@Param("runningStaleBefore") Instant runningStaleBefore);
|
@Param("runningStaleBefore") Instant runningStaleBefore);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The gate of the sync on a visit: stamps login_sync_tried_at and returns 1 only when a sync is due, so of many
|
||||||
|
* parallel requests one wins. Due: no fresh RUNNING run, and either another Steam ID, or no OK sync since
|
||||||
|
* successBefore and no try since triedBefore. 0 for a deleted or blocked user. A new row gets only the Steam ID.
|
||||||
|
* steam_id is not updated: the worker claim compares the old ID to detect an account change.
|
||||||
|
*/
|
||||||
|
@Modifying
|
||||||
|
@Transactional
|
||||||
|
@Query(value = """
|
||||||
|
INSERT INTO user_steam_sync (user_id, steam_id, login_sync_tried_at)
|
||||||
|
SELECT :userId, :steamId, :now
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM deleted_user WHERE user_id = :userId)
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM blocked_user WHERE user_id = :userId)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE
|
||||||
|
SET login_sync_tried_at = EXCLUDED.login_sync_tried_at
|
||||||
|
WHERE NOT (user_steam_sync.last_sync_status = 'RUNNING'
|
||||||
|
AND user_steam_sync.sync_started_at IS NOT NULL
|
||||||
|
AND user_steam_sync.sync_started_at >= :runningStaleBefore)
|
||||||
|
AND (user_steam_sync.steam_id <> EXCLUDED.steam_id
|
||||||
|
OR ((user_steam_sync.last_success_at IS NULL OR user_steam_sync.last_success_at < :successBefore)
|
||||||
|
AND (user_steam_sync.login_sync_tried_at IS NULL
|
||||||
|
OR user_steam_sync.login_sync_tried_at < :triedBefore)))
|
||||||
|
""", nativeQuery = true)
|
||||||
|
int tryStartLoginSync(@Param("userId") Long userId, @Param("steamId") String steamId,
|
||||||
|
@Param("now") Instant now, @Param("successBefore") Instant successBefore,
|
||||||
|
@Param("triedBefore") Instant triedBefore,
|
||||||
|
@Param("runningStaleBefore") Instant runningStaleBefore);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return 0 when another run took over the stale claim; that run's RUNNING stays
|
* @return 0 when another run took over the stale claim; that run's RUNNING stays
|
||||||
*/
|
*/
|
||||||
@@ -57,7 +87,8 @@ public interface UserSteamSyncRepository extends JpaRepository<UserSteamSync, Lo
|
|||||||
@Query(value = """
|
@Query(value = """
|
||||||
UPDATE user_steam_sync
|
UPDATE user_steam_sync
|
||||||
SET last_synced_at = :now,
|
SET last_synced_at = :now,
|
||||||
last_sync_status = :status
|
last_sync_status = :status,
|
||||||
|
last_success_at = CASE WHEN :status = 'OK' THEN :now ELSE last_success_at END
|
||||||
WHERE user_id = :userId
|
WHERE user_id = :userId
|
||||||
AND sync_started_at = :startedAt
|
AND sync_started_at = :startedAt
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -24,12 +26,14 @@ import java.util.List;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class LibrarySyncService {
|
public class LibrarySyncService {
|
||||||
|
|
||||||
|
private static final LocalTime NIGHTLY_SKIP_FROM = LocalTime.of(23, 0);
|
||||||
|
|
||||||
private final UserSteamSyncRepository userSteamSyncRepository;
|
private final UserSteamSyncRepository userSteamSyncRepository;
|
||||||
private final LibrarySyncWorker worker;
|
private final LibrarySyncWorker worker;
|
||||||
private final BacklogSnapshotRepository backlogSnapshotRepository;
|
private final BacklogSnapshotRepository backlogSnapshotRepository;
|
||||||
private final FinishedEstimateService finishedEstimateService;
|
private final FinishedEstimateService finishedEstimateService;
|
||||||
|
|
||||||
@Value("${app.library.sync-ttl-hours:24}")
|
@Value("${app.library.sync-ttl-hours:20}")
|
||||||
private long syncTtlHours;
|
private long syncTtlHours;
|
||||||
|
|
||||||
@Value("${app.library.force-sync-cooldown-seconds:60}")
|
@Value("${app.library.force-sync-cooldown-seconds:60}")
|
||||||
@@ -38,8 +42,8 @@ public class LibrarySyncService {
|
|||||||
@Value("${app.library.running-stale-minutes:5}")
|
@Value("${app.library.running-stale-minutes:5}")
|
||||||
private long runningStaleMinutes;
|
private long runningStaleMinutes;
|
||||||
|
|
||||||
@Value("${app.library.failed-retry-minutes:15}")
|
@Value("${app.library.login-retry-minutes:60}")
|
||||||
private long failedRetryMinutes;
|
private long loginRetryMinutes;
|
||||||
|
|
||||||
@Value("${app.library.nightly-active-days:14}")
|
@Value("${app.library.nightly-active-days:14}")
|
||||||
private long nightlyActiveDays;
|
private long nightlyActiveDays;
|
||||||
@@ -48,36 +52,51 @@ public class LibrarySyncService {
|
|||||||
private ZoneId zone;
|
private ZoneId zone;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trigger sync only if data is older than TTL or was synced for another Steam ID.
|
* Starts a background sync on a visit when the last OK sync is older than the TTL, at most one try per
|
||||||
* Cheap gate check on caller thread, actual sync runs async via worker.
|
* {@code login-retry-minutes} (a dropped or failed try counts too), or when the Steam ID changed.
|
||||||
|
* The check on the loaded row saves the write; the conditional upsert lets one of many parallel requests win.
|
||||||
|
*
|
||||||
|
* @return true when a sync of this visit is running or was just started, by this call or by a parallel
|
||||||
|
* request that won the gate
|
||||||
*/
|
*/
|
||||||
public void syncIfStale(Long userId, String steamId) {
|
public boolean syncIfStale(Long userId, String steamId) {
|
||||||
|
Instant now = Instant.now();
|
||||||
UserSteamSync sync = userSteamSyncRepository.findById(userId).orElse(null);
|
UserSteamSync sync = userSteamSyncRepository.findById(userId).orElse(null);
|
||||||
touchUserSeen(sync);
|
touchUserSeen(sync);
|
||||||
if (sync != null && isAlreadyRunning(sync)) {
|
Instant successBefore = now.minus(Duration.ofHours(syncTtlHours));
|
||||||
log.debug("Sync already running for user {}", userId);
|
Instant triedBefore = now.minus(Duration.ofMinutes(loginRetryMinutes));
|
||||||
return;
|
Instant runningStaleBefore = now.minus(Duration.ofMinutes(runningStaleMinutes));
|
||||||
|
if (sync != null && !isLoginSyncDue(sync, steamId, successBefore, triedBefore)) {
|
||||||
|
return isSyncActive(sync, now);
|
||||||
}
|
}
|
||||||
if (sync != null && steamId.equals(sync.getSteamId()) && !isStale(sync)) {
|
if (userSteamSyncRepository.tryStartLoginSync(userId, steamId, now, successBefore, triedBefore,
|
||||||
return;
|
runningStaleBefore) == 0) {
|
||||||
|
// Lost the gate: the winner may be a parallel request that wrote after our read.
|
||||||
|
return userSteamSyncRepository.findById(userId).map(s -> isSyncActive(s, now)).orElse(false);
|
||||||
}
|
}
|
||||||
worker.runSync(userId, steamId, SyncTrigger.AUTO);
|
worker.runSync(userId, steamId, SyncTrigger.LOGIN);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Syncs every known Steam account shortly before midnight, so the playtime history gets the growth of each
|
* Syncs every known Steam account shortly before midnight, so the playtime history gets the growth of each
|
||||||
* day even when the user does not open the app. Accounts are synced one after the other on the scheduler
|
* day even when the user does not open the app. Accounts are synced one after the other on the scheduler
|
||||||
* thread; accounts without a user request in the last {@code nightly-active-days} are skipped. Afterwards every
|
* thread; accounts without a user request in the last {@code nightly-active-days}, or with an OK sync of the
|
||||||
* user's pile size of the day goes to backlog_snapshot, and old finished games get a date from their ending
|
* same day after 23:00, are skipped (the nightly sync books the playtime growth of the
|
||||||
* achievement.
|
* day, so an earlier sync must not replace it). Afterwards every user's pile size of the day goes to backlog_snapshot, and old
|
||||||
|
* finished games get a date from their ending achievement.
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "${app.library.nightly-sync-cron:0 55 23 * * *}", zone = "${app.library.zone:Europe/Berlin}")
|
@Scheduled(cron = "${app.library.nightly-sync-cron:0 55 23 * * *}", zone = "${app.library.zone:Europe/Berlin}")
|
||||||
public void syncAllNightly() {
|
public void syncAllNightly() {
|
||||||
Instant activeSince = Instant.now().minus(Duration.ofDays(nightlyActiveDays));
|
Instant now = Instant.now();
|
||||||
List<UserSteamSync> accounts = userSteamSyncRepository.findByUserSeenAtAfter(activeSince);
|
LocalDate today = LocalDate.now(zone);
|
||||||
log.info("Nightly sync for {} Steam accounts", accounts.size());
|
List<UserSteamSync> active =
|
||||||
|
userSteamSyncRepository.findByUserSeenAtAfter(now.minus(Duration.ofDays(nightlyActiveDays)));
|
||||||
|
List<UserSteamSync> accounts =
|
||||||
|
active.stream().filter(s -> !syncedLateToday(s.getLastSuccessAt(), today)).toList();
|
||||||
|
log.info("Nightly sync for {} of {} active Steam accounts", accounts.size(), active.size());
|
||||||
// Taken before the syncs: the loop can run past midnight.
|
// Taken before the syncs: the loop can run past midnight.
|
||||||
LocalDate day = LocalDate.now(zone);
|
LocalDate day = today;
|
||||||
try {
|
try {
|
||||||
accounts.forEach(sync -> worker.runSyncNow(sync.getUserId(), sync.getSteamId(), SyncTrigger.AUTO));
|
accounts.forEach(sync -> worker.runSyncNow(sync.getUserId(), sync.getSteamId(), SyncTrigger.AUTO));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -136,12 +155,29 @@ public class LibrarySyncService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isStale(UserSteamSync sync) {
|
private boolean isLoginSyncDue(UserSteamSync sync, String steamId, Instant successBefore, Instant triedBefore) {
|
||||||
if (sync.getLastSyncedAt() == null) return true;
|
return !isAlreadyRunning(sync)
|
||||||
Duration maxAge = sync.getLastSyncStatus() == UserSteamSync.Status.FAILED
|
&& (!steamId.equals(sync.getSteamId())
|
||||||
? Duration.ofMinutes(failedRetryMinutes)
|
|| (isNullOrBefore(sync.getLastSuccessAt(), successBefore)
|
||||||
: Duration.ofHours(syncTtlHours);
|
&& isNullOrBefore(sync.getLoginSyncTriedAt(), triedBefore)));
|
||||||
return sync.getLastSyncedAt().isBefore(Instant.now().minus(maxAge));
|
}
|
||||||
|
|
||||||
|
private boolean syncedLateToday(Instant lastSuccessAt, LocalDate today) {
|
||||||
|
if (lastSuccessAt == null) return false;
|
||||||
|
ZonedDateTime at = lastSuccessAt.atZone(zone);
|
||||||
|
return at.toLocalDate().equals(today) && !at.toLocalTime().isBefore(NIGHTLY_SKIP_FROM);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A sync of this visit is running, or the gate was won and the worker has not finished (or claimed) yet.
|
||||||
|
private boolean isSyncActive(UserSteamSync sync, Instant now) {
|
||||||
|
Instant tried = sync.getLoginSyncTriedAt();
|
||||||
|
boolean justStarted = tried != null && tried.isAfter(now.minus(Duration.ofMinutes(runningStaleMinutes)))
|
||||||
|
&& (sync.getLastSyncedAt() == null || sync.getLastSyncedAt().isBefore(tried));
|
||||||
|
return isAlreadyRunning(sync) || justStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isNullOrBefore(Instant value, Instant limit) {
|
||||||
|
return value == null || value.isBefore(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isAlreadyRunning(UserSteamSync sync) {
|
private boolean isAlreadyRunning(UserSteamSync sync) {
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ app:
|
|||||||
enabled: true
|
enabled: true
|
||||||
base-url: https://www.speedrun.com/api/v1
|
base-url: https://www.speedrun.com/api/v1
|
||||||
library:
|
library:
|
||||||
sync-ttl-hours: 24
|
sync-ttl-hours: 20
|
||||||
|
# At most one sync try per user and hour on a visit, also after a failure.
|
||||||
|
login-retry-minutes: 60
|
||||||
force-sync-cooldown-seconds: 60
|
force-sync-cooldown-seconds: 60
|
||||||
running-stale-minutes: 5
|
running-stale-minutes: 5
|
||||||
achievement-ttl-hours: 1
|
achievement-ttl-hours: 1
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- last_success_at: end of the last OK sync (any trigger). login_sync_tried_at: last start of a sync on a visit.
|
||||||
|
ALTER TABLE user_steam_sync ADD COLUMN last_success_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE user_steam_sync ADD COLUMN login_sync_tried_at TIMESTAMPTZ;
|
||||||
|
UPDATE user_steam_sync SET last_success_at = last_synced_at WHERE last_sync_status = 'OK';
|
||||||
+4
-1
@@ -16,6 +16,7 @@ import java.time.Instant;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,7 +47,9 @@ class LibraryControllerSteamIdGoneTest extends AbstractIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void gamesCount_WithoutSteamIdClaim_StopsTheNightlySyncOfTheOldId() throws Exception {
|
void gamesCount_WithoutSteamIdClaim_StopsTheNightlySyncOfTheOldId() throws Exception {
|
||||||
mockMvc.perform(get("/api/library/games-count").cookie(loginWithoutSteamId())).andExpect(status().isOk());
|
mockMvc.perform(get("/api/library/games-count").cookie(loginWithoutSteamId()))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.syncStarted").value(false));
|
||||||
|
|
||||||
assertTrue(userSteamSyncRepository.findByUserSeenAtAfter(Instant.now().minus(Duration.ofDays(14))).stream()
|
assertTrue(userSteamSyncRepository.findByUserSeenAtAfter(Instant.now().minus(Duration.ofDays(14))).stream()
|
||||||
.noneMatch(sync -> sync.getUserId() == USER_ID));
|
.noneMatch(sync -> sync.getUserId() == USER_ID));
|
||||||
|
|||||||
@@ -5,8 +5,17 @@ import core_service.shared.test.AbstractPersistenceTest;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.sql.Timestamp;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -68,4 +77,188 @@ class UserSteamSyncRepositoryTest extends AbstractPersistenceTest {
|
|||||||
|
|
||||||
assertThat(claim(5L)).isZero();
|
assertThat(claim(5L)).isZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int tryLogin(long userId, String steamId) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
return repository.tryStartLoginSync(userId, steamId, now, now.minusSeconds(20 * 3600),
|
||||||
|
now.minusSeconds(3600), now.minusSeconds(300));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenRow(long userId, String steamId, String status, String startedAgo, String successAgo,
|
||||||
|
String triedAgo) {
|
||||||
|
jdbc.update("insert into user_steam_sync (user_id, steam_id, last_sync_status, sync_started_at, "
|
||||||
|
+ "last_success_at, login_sync_tried_at) values (?, ?, ?, "
|
||||||
|
+ ago(startedAgo) + ", " + ago(successAgo) + ", " + ago(triedAgo) + ")",
|
||||||
|
userId, steamId, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String ago(String interval) {
|
||||||
|
return interval == null ? "null" : "now() - interval '" + interval + "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_NoRow_CreatesItWithTheTryStamp() {
|
||||||
|
assertThat(tryLogin(10L, STEAM_ID)).isEqualTo(1);
|
||||||
|
|
||||||
|
UserSteamSync row = repository.findById(10L).orElseThrow();
|
||||||
|
assertThat(row.getSteamId()).isEqualTo(STEAM_ID);
|
||||||
|
assertThat(row.getLoginSyncTriedAt()).isNotNull();
|
||||||
|
assertThat(row.getLastSyncStatus()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_SuccessTenHoursAgo_IsRefused() {
|
||||||
|
givenRow(11L, STEAM_ID, "OK", null, "10 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(11L, STEAM_ID)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_SuccessTwentyOneHoursAgoNeverTried_Starts() {
|
||||||
|
givenRow(12L, STEAM_ID, "OK", null, "21 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(12L, STEAM_ID)).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_TriedThirtyMinutesAgo_IsRefused() {
|
||||||
|
givenRow(13L, STEAM_ID, "FAILED", null, "21 hours", "30 minutes");
|
||||||
|
|
||||||
|
assertThat(tryLogin(13L, STEAM_ID)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_TriedSixtyOneMinutesAgo_Starts() {
|
||||||
|
givenRow(14L, STEAM_ID, "FAILED", null, "21 hours", "61 minutes");
|
||||||
|
|
||||||
|
assertThat(tryLogin(14L, STEAM_ID)).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_SecondCallAfterAWinner_IsRefused() {
|
||||||
|
givenRow(15L, STEAM_ID, "OK", null, "21 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(15L, STEAM_ID)).isEqualTo(1);
|
||||||
|
assertThat(tryLogin(15L, STEAM_ID)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
||||||
|
void tryStartLoginSync_ParallelCallsOnAStaleRow_OnlyOneWins() throws Exception {
|
||||||
|
long userId = 16L;
|
||||||
|
jdbc.update("delete from user_steam_sync where user_id = ?", userId);
|
||||||
|
givenRow(userId, STEAM_ID, "OK", null, "21 hours", null);
|
||||||
|
int callers = 8;
|
||||||
|
ExecutorService pool = Executors.newFixedThreadPool(callers);
|
||||||
|
CountDownLatch go = new CountDownLatch(1);
|
||||||
|
try {
|
||||||
|
List<Future<Integer>> results = new ArrayList<>();
|
||||||
|
for (int i = 0; i < callers; i++) {
|
||||||
|
results.add(pool.submit(() -> {
|
||||||
|
go.await();
|
||||||
|
return tryLogin(userId, STEAM_ID);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
go.countDown();
|
||||||
|
int wins = 0;
|
||||||
|
for (Future<Integer> result : results) {
|
||||||
|
wins += result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(wins).isEqualTo(1);
|
||||||
|
} finally {
|
||||||
|
pool.shutdownNow();
|
||||||
|
jdbc.update("delete from user_steam_sync where user_id = ?", userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_FreshRunning_IsRefused() {
|
||||||
|
givenRow(17L, STEAM_ID, "RUNNING", "10 seconds", "21 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(17L, STEAM_ID)).isZero();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_RunningStartedTwentyMinutesAgo_Starts() {
|
||||||
|
givenRow(18L, STEAM_ID, "RUNNING", "20 minutes", "21 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(18L, STEAM_ID)).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_RunningWithoutStartTime_Starts() {
|
||||||
|
givenRow(19L, STEAM_ID, "RUNNING", null, "21 hours", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(19L, STEAM_ID)).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_FreshSuccessButOtherSteamId_StartsAndKeepsTheOldId() {
|
||||||
|
givenRow(20L, "76561198005679790", "OK", null, "1 hour", null);
|
||||||
|
|
||||||
|
assertThat(tryLogin(20L, STEAM_ID)).isEqualTo(1);
|
||||||
|
assertThat(repository.findById(20L).orElseThrow().getSteamId()).isEqualTo("76561198005679790");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_FailedFirstSyncOfANewSteamId_RetriesAfterAnHour() {
|
||||||
|
givenRow(26L, "76561198005679790", "OK", null, "1 hour", null);
|
||||||
|
assertThat(tryLogin(26L, STEAM_ID)).isEqualTo(1);
|
||||||
|
Instant started = Instant.now();
|
||||||
|
repository.tryClaimRun(26L, STEAM_ID, started, started.minusSeconds(300));
|
||||||
|
repository.markFinished(26L, started, "FAILED", Instant.now());
|
||||||
|
|
||||||
|
Instant inAnHour = Instant.now().plusSeconds(61 * 60);
|
||||||
|
int started2 = repository.tryStartLoginSync(26L, STEAM_ID, inAnHour, inAnHour.minusSeconds(20 * 3600),
|
||||||
|
inAnHour.minusSeconds(3600), inAnHour.minusSeconds(300));
|
||||||
|
|
||||||
|
assertThat(started2).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_DeletedUser_IsRefusedWithoutARow() {
|
||||||
|
jdbc.update("insert into deleted_user (user_id) values (21)");
|
||||||
|
|
||||||
|
assertThat(tryLogin(21L, STEAM_ID)).isZero();
|
||||||
|
assertThat(repository.findById(21L)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tryStartLoginSync_BlockedUser_IsRefusedWithoutARow() {
|
||||||
|
jdbc.update("insert into blocked_user (user_id) values (22)");
|
||||||
|
|
||||||
|
assertThat(tryLogin(22L, STEAM_ID)).isZero();
|
||||||
|
assertThat(repository.findById(22L)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant lastSuccessAt(long userId) {
|
||||||
|
Timestamp value = jdbc.queryForObject(
|
||||||
|
"select last_success_at from user_steam_sync where user_id = ?", Timestamp.class, userId);
|
||||||
|
return value == null ? null : value.toInstant();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markFinished_Ok_SetsLastSuccessAt() {
|
||||||
|
Instant started = Instant.now();
|
||||||
|
repository.tryClaimRun(23L, STEAM_ID, started, started.minusSeconds(600));
|
||||||
|
|
||||||
|
repository.markFinished(23L, started, "OK", Instant.now());
|
||||||
|
|
||||||
|
assertThat(lastSuccessAt(23L)).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markFinished_Failed_KeepsLastSuccessAt() {
|
||||||
|
Instant started = Instant.now();
|
||||||
|
repository.tryClaimRun(24L, STEAM_ID, started, started.minusSeconds(600));
|
||||||
|
repository.markFinished(24L, started, "OK", Instant.now().minusSeconds(3600));
|
||||||
|
Instant success = lastSuccessAt(24L);
|
||||||
|
Instant secondRun = Instant.now();
|
||||||
|
repository.tryClaimRun(24L, STEAM_ID, secondRun, secondRun.minusSeconds(600));
|
||||||
|
|
||||||
|
repository.markFinished(24L, secondRun, "FAILED", Instant.now());
|
||||||
|
|
||||||
|
assertThat(lastSuccessAt(24L)).isEqualTo(success);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,9 @@ import java.time.ZoneId;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
@@ -53,72 +55,107 @@ class LibrarySyncServiceTest extends AbstractUnitTest {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
ReflectionTestUtils.setField(syncService, "syncTtlHours", 24L);
|
ReflectionTestUtils.setField(syncService, "syncTtlHours", 20L);
|
||||||
ReflectionTestUtils.setField(syncService, "forceSyncCooldownSeconds", 60L);
|
ReflectionTestUtils.setField(syncService, "forceSyncCooldownSeconds", 60L);
|
||||||
ReflectionTestUtils.setField(syncService, "runningStaleMinutes", 5L);
|
ReflectionTestUtils.setField(syncService, "runningStaleMinutes", 5L);
|
||||||
ReflectionTestUtils.setField(syncService, "failedRetryMinutes", 15L);
|
ReflectionTestUtils.setField(syncService, "loginRetryMinutes", 60L);
|
||||||
ReflectionTestUtils.setField(syncService, "zone", ZoneId.of("Europe/Berlin"));
|
ReflectionTestUtils.setField(syncService, "zone", ZoneId.of("Europe/Berlin"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void givenSync(String steamId, UserSteamSync.Status status, Duration age) {
|
private void givenSync(String steamId, UserSteamSync.Status status, Duration successAge, Duration triedAge) {
|
||||||
UserSteamSync sync = new UserSteamSync(USER_ID, steamId);
|
UserSteamSync sync = new UserSteamSync(USER_ID, steamId);
|
||||||
sync.setLastSyncStatus(status);
|
sync.setLastSyncStatus(status);
|
||||||
sync.setLastSyncedAt(Instant.now().minus(age));
|
sync.setLastSyncedAt(Instant.now());
|
||||||
sync.setSyncStartedAt(Instant.now().minus(age));
|
sync.setSyncStartedAt(Instant.now().minus(Duration.ofSeconds(10)));
|
||||||
|
sync.setLastSuccessAt(successAge == null ? null : Instant.now().minus(successAge));
|
||||||
|
sync.setLoginSyncTriedAt(triedAge == null ? null : Instant.now().minus(triedAge));
|
||||||
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(sync));
|
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(sync));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void givenGate(int result) {
|
||||||
|
when(userSteamSyncRepository.tryStartLoginSync(eq(USER_ID), eq(STEAM_ID), any(), any(), any(), any()))
|
||||||
|
.thenReturn(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyGateNotCalled() {
|
||||||
|
verify(userSteamSyncRepository, never()).tryStartLoginSync(anyLong(), anyString(), any(), any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void syncIfStale_NoSyncYet_Runs() {
|
void syncIfStale_NoSyncYet_AsksTheGateAndRuns() {
|
||||||
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.empty());
|
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.empty());
|
||||||
|
givenGate(1);
|
||||||
|
|
||||||
syncService.syncIfStale(USER_ID, STEAM_ID);
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void syncIfStale_FreshOkSync_Skips() {
|
void syncIfStale_SuccessNineteenHoursAgo_SkipsWithoutAWrite() {
|
||||||
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofMinutes(10));
|
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofHours(19), null);
|
||||||
|
|
||||||
syncService.syncIfStale(USER_ID, STEAM_ID);
|
assertFalse(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
|
verifyGateNotCalled();
|
||||||
|
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_StaleButTriedThirtyMinutesAgo_SkipsWithoutAWrite() {
|
||||||
|
givenSync(STEAM_ID, UserSteamSync.Status.FAILED, Duration.ofHours(21), Duration.ofMinutes(30));
|
||||||
|
|
||||||
|
assertFalse(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
|
verifyGateNotCalled();
|
||||||
|
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_StaleAndTriedLongAgo_GateWins_RunsALoginSync() {
|
||||||
|
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofHours(21), Duration.ofMinutes(61));
|
||||||
|
givenGate(1);
|
||||||
|
|
||||||
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
|
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.LOGIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_NeverSucceeded_GateWins_Runs() {
|
||||||
|
givenSync(STEAM_ID, UserSteamSync.Status.FAILED, null, null);
|
||||||
|
givenGate(1);
|
||||||
|
|
||||||
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_GateLoses_DoesNotRun() {
|
||||||
|
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofHours(21), null);
|
||||||
|
givenGate(0);
|
||||||
|
|
||||||
|
assertFalse(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void syncIfStale_FreshSyncForOtherSteamId_Runs() {
|
void syncIfStale_FreshSuccessForOtherSteamId_AsksTheGate() {
|
||||||
givenSync("76561198005679790", UserSteamSync.Status.OK, Duration.ofMinutes(10));
|
givenSync("76561198005679790", UserSteamSync.Status.OK, Duration.ofHours(1), Duration.ofMinutes(5));
|
||||||
|
givenGate(1);
|
||||||
|
|
||||||
syncService.syncIfStale(USER_ID, STEAM_ID);
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void syncIfStale_FailedSyncAfterRetryInterval_Runs() {
|
void syncIfStale_RunningForOtherSteamId_SkipsTheGateAndReportsTheRunningSync() {
|
||||||
givenSync(STEAM_ID, UserSteamSync.Status.FAILED, Duration.ofMinutes(16));
|
givenSync("76561198005679790", UserSteamSync.Status.RUNNING, Duration.ofHours(30), null);
|
||||||
|
|
||||||
syncService.syncIfStale(USER_ID, STEAM_ID);
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
verify(worker).runSync(USER_ID, STEAM_ID, SyncTrigger.AUTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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(), any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void syncIfStale_RunningForOtherSteamId_Skips() {
|
|
||||||
givenSync("76561198005679790", UserSteamSync.Status.RUNNING, Duration.ofSeconds(10));
|
|
||||||
|
|
||||||
syncService.syncIfStale(USER_ID, STEAM_ID);
|
|
||||||
|
|
||||||
|
verifyGateNotCalled();
|
||||||
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +173,54 @@ class LibrarySyncServiceTest extends AbstractUnitTest {
|
|||||||
order.verify(finishedEstimateService).fillMissing();
|
order.verify(finishedEstimateService).fillMissing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncAllNightly_SkipsOnlyAccountsWithAnOkSyncOfTodayAfter23() {
|
||||||
|
ZoneId berlin = ZoneId.of("Europe/Berlin");
|
||||||
|
LocalDate today = LocalDate.now(berlin);
|
||||||
|
UserSteamSync late = new UserSteamSync(1L, "76561198000000001");
|
||||||
|
late.setLastSuccessAt(today.atTime(23, 0).atZone(berlin).toInstant());
|
||||||
|
UserSteamSync evening = new UserSteamSync(2L, "76561198005679790");
|
||||||
|
evening.setLastSuccessAt(today.atTime(22, 59).atZone(berlin).toInstant());
|
||||||
|
UserSteamSync yesterdayLate = new UserSteamSync(3L, "76561198005679791");
|
||||||
|
yesterdayLate.setLastSuccessAt(today.minusDays(1).atTime(23, 30).atZone(berlin).toInstant());
|
||||||
|
UserSteamSync never = new UserSteamSync(4L, "76561198005679792");
|
||||||
|
when(userSteamSyncRepository.findByUserSeenAtAfter(any()))
|
||||||
|
.thenReturn(List.of(late, evening, yesterdayLate, never));
|
||||||
|
|
||||||
|
syncService.syncAllNightly();
|
||||||
|
|
||||||
|
verify(worker, never()).runSyncNow(eq(1L), anyString(), any());
|
||||||
|
verify(worker).runSyncNow(2L, "76561198005679790", SyncTrigger.AUTO);
|
||||||
|
verify(worker).runSyncNow(3L, "76561198005679791", SyncTrigger.AUTO);
|
||||||
|
verify(worker).runSyncNow(4L, "76561198005679792", SyncTrigger.AUTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_GateLost_OtherRequestJustStartedTheSync_ReportsItAsStarted() {
|
||||||
|
UserSteamSync before = new UserSteamSync(USER_ID, STEAM_ID);
|
||||||
|
before.setLastSuccessAt(Instant.now().minus(Duration.ofHours(21)));
|
||||||
|
UserSteamSync after = new UserSteamSync(USER_ID, STEAM_ID);
|
||||||
|
after.setLastSuccessAt(before.getLastSuccessAt());
|
||||||
|
after.setLoginSyncTriedAt(Instant.now());
|
||||||
|
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(before), Optional.of(after));
|
||||||
|
givenGate(0);
|
||||||
|
|
||||||
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
|
verify(worker, never()).runSync(anyLong(), anyString(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void syncIfStale_TriedJustNowAndWorkerNotClaimedYet_ReportsItAsStarted() {
|
||||||
|
givenSync(STEAM_ID, UserSteamSync.Status.OK, Duration.ofHours(21), Duration.ofSeconds(1));
|
||||||
|
UserSteamSync sync = userSteamSyncRepository.findById(USER_ID).orElseThrow();
|
||||||
|
sync.setLastSyncedAt(Instant.now().minus(Duration.ofHours(21)));
|
||||||
|
|
||||||
|
assertTrue(syncService.syncIfStale(USER_ID, STEAM_ID));
|
||||||
|
|
||||||
|
verifyGateNotCalled();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void syncAllNightly_SnapshotFails_TheFinishDatesAreStillEstimated() {
|
void syncAllNightly_SnapshotFails_TheFinishDatesAreStillEstimated() {
|
||||||
when(userSteamSyncRepository.findByUserSeenAtAfter(any())).thenReturn(List.of());
|
when(userSteamSyncRepository.findByUserSeenAtAfter(any())).thenReturn(List.of());
|
||||||
@@ -174,11 +259,11 @@ class LibrarySyncServiceTest extends AbstractUnitTest {
|
|||||||
void syncIfStale_MarksTheUserAsSeenAtMostHourly() {
|
void syncIfStale_MarksTheUserAsSeenAtMostHourly() {
|
||||||
UserSteamSync seenLongAgo = new UserSteamSync(USER_ID, STEAM_ID);
|
UserSteamSync seenLongAgo = new UserSteamSync(USER_ID, STEAM_ID);
|
||||||
seenLongAgo.setLastSyncStatus(UserSteamSync.Status.OK);
|
seenLongAgo.setLastSyncStatus(UserSteamSync.Status.OK);
|
||||||
seenLongAgo.setLastSyncedAt(Instant.now());
|
seenLongAgo.setLastSuccessAt(Instant.now());
|
||||||
seenLongAgo.setUserSeenAt(Instant.now().minus(Duration.ofDays(2)));
|
seenLongAgo.setUserSeenAt(Instant.now().minus(Duration.ofDays(2)));
|
||||||
UserSteamSync seenJustNow = new UserSteamSync(USER_ID, STEAM_ID);
|
UserSteamSync seenJustNow = new UserSteamSync(USER_ID, STEAM_ID);
|
||||||
seenJustNow.setLastSyncStatus(UserSteamSync.Status.OK);
|
seenJustNow.setLastSyncStatus(UserSteamSync.Status.OK);
|
||||||
seenJustNow.setLastSyncedAt(Instant.now());
|
seenJustNow.setLastSuccessAt(Instant.now());
|
||||||
seenJustNow.setUserSeenAt(Instant.now());
|
seenJustNow.setUserSeenAt(Instant.now());
|
||||||
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(seenLongAgo), Optional.of(seenJustNow));
|
when(userSteamSyncRepository.findById(USER_ID)).thenReturn(Optional.of(seenLongAgo), Optional.of(seenJustNow));
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,15 @@ class LibrarySyncWorkerTest extends AbstractUnitTest {
|
|||||||
verify(userSteamSyncRepository).markFinished(eq(USER_ID), any(), eq("FAILED"), any());
|
verify(userSteamSyncRepository).markFinished(eq(USER_ID), any(), eq("FAILED"), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runSync_LoginTrigger_WritesTheResultWithThatTrigger() {
|
||||||
|
steamReturns(game(100, 0), null);
|
||||||
|
|
||||||
|
worker.runSync(USER_ID, STEAM_ID, SyncTrigger.LOGIN);
|
||||||
|
|
||||||
|
assertThat(replacedChanges(SyncTrigger.LOGIN, 1)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void runSync_WithGames_MarksOk() {
|
void runSync_WithGames_MarksOk() {
|
||||||
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any(), any())).thenReturn(1);
|
when(userSteamSyncRepository.tryClaimRun(eq(USER_ID), eq(STEAM_ID), any(), any())).thenReturn(1);
|
||||||
|
|||||||
Reference in new issue
Block a user