Public Access
test: background work inline, shared mock bases, lock-wait instead of sleep
This commit is contained in:
1 parent
7fe2823519
commit
3512159b41
18 files changed
+196
-156
No files matched your search
@@ -437,6 +437,10 @@ Configuration comes from environment variables:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers.
|
Runs unit, WebMvc, persistence and integration tests plus the ArchUnit rules. Needs Docker for Testcontainers.
|
||||||
|
In integration tests background work runs on the calling thread, so a test reads the result at once.
|
||||||
|
`AbstractMockedAdaptersTest` mocks Steam, IGDB and speedrun.com, `AbstractMockedLibraryServicesTest` the sync and
|
||||||
|
the refreshes of the picker and stats pages; each is one shared Spring context. Race tests wait until the other
|
||||||
|
connection waits for the row lock (`awaitLockWaiter`), not for a fixed time.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package core_service.configuration.security;
|
package core_service.configuration.security;
|
||||||
|
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
@@ -16,7 +16,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile logs every request in, so only the CSRF header and CORS decide here.
|
* The dev profile logs every request in, so only the CSRF header and CORS decide here.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class ClientHeaderFilterTest extends AbstractIntegrationTest {
|
class ClientHeaderFilterTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final String FRONTEND = "https://tbp.andreas-dahm.eu";
|
private static final String FRONTEND = "https://tbp.andreas-dahm.eu";
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import core_service.modules.library.domain.UserGame;
|
|||||||
import core_service.modules.library.domain.UserGameId;
|
import core_service.modules.library.domain.UserGameId;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.library.service.GameImportWorker;
|
import core_service.modules.library.service.GameImportWorker;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import com.jayway.jsonpath.JsonPath;
|
import com.jayway.jsonpath.JsonPath;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -16,7 +16,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
import org.springframework.test.web.servlet.ResultActions;
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class GameImportControllerTest extends AbstractIntegrationTest {
|
class GameImportControllerTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final IgdbApiService.Game ZELDA = new IgdbApiService.Game(119388, "Zelda: Tears of the Kingdom", 2023,
|
private static final IgdbApiService.Game ZELDA = new IgdbApiService.Game(119388, "Zelda: Tears of the Kingdom", 2023,
|
||||||
@@ -70,9 +69,6 @@ class GameImportControllerTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JdbcTemplate jdbc;
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private IgdbApiService igdbApiService;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void seed() {
|
void seed() {
|
||||||
jdbc.update("delete from game_alias");
|
jdbc.update("delete from game_alias");
|
||||||
@@ -104,7 +100,7 @@ class GameImportControllerTest extends AbstractIntegrationTest {
|
|||||||
awaitStatus(id, "READY");
|
awaitStatus(id, "READY");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The matching and the save run in the background.
|
// The matching and the save run on the worker's own lane threads, not on the test executor, so the test polls.
|
||||||
private void awaitStatus(long id, String expected) throws InterruptedException {
|
private void awaitStatus(long id, String expected) throws InterruptedException {
|
||||||
for (int attempt = 0; attempt < 50; attempt++) {
|
for (int attempt = 0; attempt < 50; attempt++) {
|
||||||
if (expected.equals(jdbc.queryForObject("select status from game_import where id = ?", String.class, id))) {
|
if (expected.equals(jdbc.queryForObject("select status from game_import where id = ?", String.class, id))) {
|
||||||
|
|||||||
+3
-9
@@ -10,13 +10,11 @@ import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
|||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.library.service.LibrarySyncWorker;
|
import core_service.modules.library.service.LibrarySyncWorker;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -27,7 +25,6 @@ import static org.hamcrest.Matchers.hasSize;
|
|||||||
import static org.hamcrest.Matchers.nullValue;
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.timeout;
|
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
@@ -38,7 +35,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class LibraryControllerAchievementsTest extends AbstractIntegrationTest {
|
class LibraryControllerAchievementsTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final String REPAIRED_COVER = "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1/x/header.jpg";
|
private static final String REPAIRED_COVER = "https://shared.akamai.steamstatic.com/store_item_assets/steam/apps/1/x/header.jpg";
|
||||||
@@ -46,9 +43,6 @@ class LibraryControllerAchievementsTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SteamApiService steamApiService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SteamGameRepository steamGameRepository;
|
private SteamGameRepository steamGameRepository;
|
||||||
|
|
||||||
@@ -161,6 +155,6 @@ class LibraryControllerAchievementsTest extends AbstractIntegrationTest {
|
|||||||
|
|
||||||
librarySyncWorker.runSyncNow(USER_ID, "76561198000000001");
|
librarySyncWorker.runSyncNow(USER_ID, "76561198000000001");
|
||||||
|
|
||||||
verify(steamApiService, timeout(5_000)).getSchemaForGame(620L);
|
verify(steamApiService).getSchemaForGame(620L);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ import core_service.modules.library.domain.UserGame;
|
|||||||
import core_service.modules.library.domain.UserGameId;
|
import core_service.modules.library.domain.UserGameId;
|
||||||
import core_service.modules.library.repo.SteamGameRepository;
|
import core_service.modules.library.repo.SteamGameRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
@@ -34,7 +34,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class LibraryControllerHiddenTest extends AbstractIntegrationTest {
|
class LibraryControllerHiddenTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
|
|
||||||
|
|||||||
@@ -11,19 +11,14 @@ import core_service.modules.library.repo.GameSpeedrunRepository;
|
|||||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||||
import core_service.modules.library.repo.SteamGameRepository;
|
import core_service.modules.library.repo.SteamGameRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.library.service.LibrarySyncService;
|
import core_service.shared.test.AbstractMockedLibraryServicesTest;
|
||||||
import core_service.modules.library.service.MoodService;
|
|
||||||
import core_service.modules.library.service.SpeedrunService;
|
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.hasSize;
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
@@ -37,7 +32,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class LibraryControllerPickerTest extends AbstractIntegrationTest {
|
class LibraryControllerPickerTest extends AbstractMockedLibraryServicesTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
|
|
||||||
@@ -56,15 +51,6 @@ class LibraryControllerPickerTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private GameMoodsRepository gameMoodsRepository;
|
private GameMoodsRepository gameMoodsRepository;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private LibrarySyncService syncService;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private MoodService moodService;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SpeedrunService speedrunService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private GameSpeedrunRepository gameSpeedrunRepository;
|
private GameSpeedrunRepository gameSpeedrunRepository;
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import core_service.modules.library.domain.UserGame;
|
|||||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||||
import core_service.modules.library.repo.SteamGameRepository;
|
import core_service.modules.library.repo.SteamGameRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.library.service.LibrarySyncService;
|
import core_service.shared.test.AbstractMockedLibraryServicesTest;
|
||||||
import core_service.modules.library.service.MoodService;
|
|
||||||
import core_service.modules.library.service.SpeedrunService;
|
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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;
|
||||||
@@ -20,7 +17,6 @@ import org.springframework.boot.test.system.CapturedOutput;
|
|||||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -43,7 +39,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class LibraryControllerStatsTest extends AbstractIntegrationTest {
|
class LibraryControllerStatsTest extends AbstractMockedLibraryServicesTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
|
|
||||||
@@ -59,16 +55,6 @@ class LibraryControllerStatsTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||||
|
|
||||||
// The same mocks as LibraryControllerPickerTest, so both share one Spring context.
|
|
||||||
@MockitoBean
|
|
||||||
private LibrarySyncService syncService;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private MoodService moodService;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SpeedrunService speedrunService;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void seed() {
|
void seed() {
|
||||||
userGameRepository.deleteAll();
|
userGameRepository.deleteAll();
|
||||||
|
|||||||
+2
-7
@@ -1,12 +1,10 @@
|
|||||||
package core_service.modules.library.api;
|
package core_service.modules.library.api;
|
||||||
|
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerSummaryResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
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.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -21,14 +19,11 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1 with Steam ID 76561198000000001.
|
* The dev profile authenticates every request as user 1 with Steam ID 76561198000000001.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class LibraryControllerSteamProfileTest extends AbstractIntegrationTest {
|
class LibraryControllerSteamProfileTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MockMvc mockMvc;
|
private MockMvc mockMvc;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SteamApiService steamApiService;
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void steamProfile_SendsNameAndAvatar() throws Exception {
|
void steamProfile_SendsNameAndAvatar() throws Exception {
|
||||||
when(steamApiService.getPlayerSummary("76561198000000001")).thenReturn(Optional.of(new SteamPlayerSummaryResponse.Player(
|
when(steamApiService.getPlayerSummary("76561198000000001")).thenReturn(Optional.of(new SteamPlayerSummaryResponse.Player(
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ import core_service.configuration.security.ClientHeaderFilter;
|
|||||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||||
import core_service.modules.library.domain.GameSource;
|
import core_service.modules.library.domain.GameSource;
|
||||||
import core_service.modules.library.domain.GameStatus;
|
import core_service.modules.library.domain.GameStatus;
|
||||||
import core_service.modules.library.domain.GameTimeToBeat;
|
|
||||||
import core_service.modules.library.domain.SteamGame;
|
import core_service.modules.library.domain.SteamGame;
|
||||||
import core_service.modules.library.domain.UserGame;
|
import core_service.modules.library.domain.UserGame;
|
||||||
import core_service.modules.library.domain.UserGameId;
|
import core_service.modules.library.domain.UserGameId;
|
||||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||||
import core_service.modules.library.repo.SteamGameRepository;
|
import core_service.modules.library.repo.SteamGameRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.ContractTokens;
|
import core_service.shared.test.ContractTokens;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -19,7 +18,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -48,7 +46,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
* The dev profile authenticates every request as user 1.
|
* The dev profile authenticates every request as user 1.
|
||||||
*/
|
*/
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
class ManualGameControllerTest extends AbstractIntegrationTest {
|
class ManualGameControllerTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final IgdbApiService.Game ZELDA = new IgdbApiService.Game(119388, "Zelda: Tears of the Kingdom", 2023,
|
private static final IgdbApiService.Game ZELDA = new IgdbApiService.Game(119388, "Zelda: Tears of the Kingdom", 2023,
|
||||||
@@ -71,9 +69,6 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JdbcTemplate jdbc;
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private IgdbApiService igdbApiService;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void seed() {
|
void seed() {
|
||||||
jdbc.update("delete from game_alias");
|
jdbc.update("delete from game_alias");
|
||||||
@@ -115,19 +110,7 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
|
|||||||
SteamGame catalog = steamGameRepository.findById(-119388L).orElseThrow();
|
SteamGame catalog = steamGameRepository.findById(-119388L).orElseThrow();
|
||||||
assertEquals("Zelda: Tears of the Kingdom", catalog.getName());
|
assertEquals("Zelda: Tears of the Kingdom", catalog.getName());
|
||||||
assertEquals(ZELDA.imageUrl(), catalog.getImageUrl());
|
assertEquals(ZELDA.imageUrl(), catalog.getImageUrl());
|
||||||
assertEquals(3600, timeToBeatStored(-119388L).getNormallyMinutes());
|
assertEquals(3600, gameTimeToBeatRepository.findById(-119388L).orElseThrow().getNormallyMinutes());
|
||||||
}
|
|
||||||
|
|
||||||
// The add asks IGDB for the time to beat in the background.
|
|
||||||
private GameTimeToBeat timeToBeatStored(long appid) throws InterruptedException {
|
|
||||||
for (int attempt = 0; attempt < 50; attempt++) {
|
|
||||||
Optional<GameTimeToBeat> found = gameTimeToBeatRepository.findById(appid);
|
|
||||||
if (found.isPresent()) {
|
|
||||||
return found.get();
|
|
||||||
}
|
|
||||||
Thread.sleep(100);
|
|
||||||
}
|
|
||||||
throw new AssertionError("No time to beat stored for appid " + appid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -333,7 +316,7 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void gameAddedByHand_ShowsUpInTheLibraryAndItsDetail() throws Exception {
|
void gameAddedByHand_ShowsUpInTheLibraryAndItsDetail() throws Exception {
|
||||||
add(119388, "Switch");
|
add(119388, "Switch");
|
||||||
timeToBeatStored(-119388L);
|
assertTrue(gameTimeToBeatRepository.findById(-119388L).isPresent());
|
||||||
|
|
||||||
mockMvc.perform(get("/api/library/games").param("q", "zelda"))
|
mockMvc.perform(get("/api/library/games").param("q", "zelda"))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
|
|||||||
+7
-19
@@ -3,13 +3,11 @@ package core_service.modules.library.service;
|
|||||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||||
import core_service.modules.library.repo.UserGameAchievementRepository;
|
import core_service.modules.library.repo.UserGameAchievementRepository;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ import static org.mockito.Mockito.when;
|
|||||||
* The game detail page refreshes inside its own @Transactional while the background sync refreshes the same
|
* The game detail page refreshes inside its own @Transactional while the background sync refreshes the same
|
||||||
* (user, appid) in a transaction of its own, e.g. right after the user pressed "sync" and opened the most played game.
|
* (user, appid) in a transaction of its own, e.g. right after the user pressed "sync" and opened the most played game.
|
||||||
*/
|
*/
|
||||||
class AchievementSyncServiceConcurrencyTest extends AbstractIntegrationTest {
|
class AchievementSyncServiceConcurrencyTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final String STEAM_ID = "76561198000000001";
|
private static final String STEAM_ID = "76561198000000001";
|
||||||
@@ -45,9 +43,6 @@ class AchievementSyncServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PlatformTransactionManager transactionManager;
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SteamApiService steamApiService;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void clean() {
|
void clean() {
|
||||||
@@ -61,20 +56,20 @@ class AchievementSyncServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
new SteamPlayerAchievementsResponse.PlayerStats(STEAM_ID, "Portal 2",
|
new SteamPlayerAchievementsResponse.PlayerStats(STEAM_ID, "Portal 2",
|
||||||
List.of(new SteamPlayerAchievementsResponse.Achievement("A", 1, 1_700_000_000L)), true, null)));
|
List.of(new SteamPlayerAchievementsResponse.Achievement("A", 1, 1_700_000_000L)), true, null)));
|
||||||
CountDownLatch firstWrote = new CountDownLatch(1);
|
CountDownLatch firstWrote = new CountDownLatch(1);
|
||||||
CountDownLatch secondStarted = new CountDownLatch(1);
|
CountDownLatch secondDone = new CountDownLatch(1);
|
||||||
|
|
||||||
CompletableFuture<Void> detailPage = CompletableFuture.runAsync(() -> new TransactionTemplate(transactionManager)
|
CompletableFuture<Void> detailPage = CompletableFuture.runAsync(() -> new TransactionTemplate(transactionManager)
|
||||||
.executeWithoutResult(tx -> {
|
.executeWithoutResult(tx -> {
|
||||||
achievementSyncService.refreshUserIfStale(USER_ID, STEAM_ID, 620L, 60, Duration.ofHours(1));
|
achievementSyncService.refreshUserIfStale(USER_ID, STEAM_ID, 620L, 60, Duration.ofHours(1));
|
||||||
firstWrote.countDown();
|
firstWrote.countDown();
|
||||||
await(secondStarted);
|
// The page's transaction stays open until the background run wrote the same rows; a run that
|
||||||
// Keep the page's transaction open while the background run writes the same rows.
|
// waited for this transaction would time out the latch.
|
||||||
sleep();
|
await(secondDone);
|
||||||
}));
|
}));
|
||||||
CompletableFuture<Void> backgroundRun = CompletableFuture.runAsync(() -> {
|
CompletableFuture<Void> backgroundRun = CompletableFuture.runAsync(() -> {
|
||||||
await(firstWrote);
|
await(firstWrote);
|
||||||
secondStarted.countDown();
|
|
||||||
achievementSyncService.refreshUserIfStale(USER_ID, STEAM_ID, 620L, 60, Duration.ofHours(1));
|
achievementSyncService.refreshUserIfStale(USER_ID, STEAM_ID, 620L, 60, Duration.ofHours(1));
|
||||||
|
secondDone.countDown();
|
||||||
});
|
});
|
||||||
|
|
||||||
assertDoesNotThrow(() -> CompletableFuture.allOf(detailPage, backgroundRun).get(30, TimeUnit.SECONDS));
|
assertDoesNotThrow(() -> CompletableFuture.allOf(detailPage, backgroundRun).get(30, TimeUnit.SECONDS));
|
||||||
@@ -90,11 +85,4 @@ class AchievementSyncServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sleep() {
|
|
||||||
try {
|
|
||||||
Thread.sleep(500);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
+2
-10
@@ -55,12 +55,12 @@ class GameCorrectionServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
void twoAuditorsCorrectTheSameFieldForTheFirstTime_BothSucceed() {
|
void twoAuditorsCorrectTheSameFieldForTheFirstTime_BothSucceed() {
|
||||||
CountDownLatch firstCorrected = new CountDownLatch(1);
|
CountDownLatch firstCorrected = new CountDownLatch(1);
|
||||||
|
|
||||||
// The first commits a moment after the second has started, so both see no row before either commits.
|
// The first commits only when the second waits for its row, so both saw no row before either commits.
|
||||||
CompletableFuture<Void> first = CompletableFuture.runAsync(() -> new TransactionTemplate(transactionManager)
|
CompletableFuture<Void> first = CompletableFuture.runAsync(() -> new TransactionTemplate(transactionManager)
|
||||||
.executeWithoutResult(tx -> {
|
.executeWithoutResult(tx -> {
|
||||||
gameCorrectionService.correct(GAME, "name", "Portal Two", 1L);
|
gameCorrectionService.correct(GAME, "name", "Portal Two", 1L);
|
||||||
firstCorrected.countDown();
|
firstCorrected.countDown();
|
||||||
pause();
|
awaitLockWaiter(jdbcTemplate);
|
||||||
}));
|
}));
|
||||||
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
|
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
|
||||||
await(firstCorrected);
|
await(firstCorrected);
|
||||||
@@ -86,14 +86,6 @@ class GameCorrectionServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
assertThat(steamGameRepository.findById(GAME).orElseThrow().getImageUrl()).isEqualTo(IMAGE);
|
assertThat(steamGameRepository.findById(GAME).orElseThrow().getImageUrl()).isEqualTo(IMAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void pause() {
|
|
||||||
try {
|
|
||||||
Thread.sleep(300);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void await(CountDownLatch latch) {
|
private static void await(CountDownLatch latch) {
|
||||||
try {
|
try {
|
||||||
if (!latch.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("latch timed out");
|
if (!latch.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("latch timed out");
|
||||||
|
|||||||
@@ -11,13 +11,11 @@ import core_service.modules.library.repo.SteamGameRepository;
|
|||||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.test.context.TestPropertySource;
|
import org.springframework.test.context.TestPropertySource;
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -32,7 +30,7 @@ import static org.mockito.Mockito.when;
|
|||||||
@TestPropertySource(properties = {
|
@TestPropertySource(properties = {
|
||||||
"spring.datasource.hikari.maximum-pool-size=1",
|
"spring.datasource.hikari.maximum-pool-size=1",
|
||||||
"spring.datasource.hikari.connection-timeout=1000"})
|
"spring.datasource.hikari.connection-timeout=1000"})
|
||||||
class GameDetailServicePoolTest extends AbstractIntegrationTest {
|
class GameDetailServicePoolTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final String STEAM_ID = "76561198000000001";
|
private static final String STEAM_ID = "76561198000000001";
|
||||||
@@ -52,9 +50,6 @@ class GameDetailServicePoolTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SteamApiService steamApiService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private GameAchievementSchemaRepository schemaRepository;
|
private GameAchievementSchemaRepository schemaRepository;
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,25 @@ import core_service.modules.library.repo.GameAchievementSchemaMetaRepository;
|
|||||||
import core_service.modules.library.repo.SteamGameRepository;
|
import core_service.modules.library.repo.SteamGameRepository;
|
||||||
import core_service.modules.library.repo.GameAchievementSchemaRepository;
|
import core_service.modules.library.repo.GameAchievementSchemaRepository;
|
||||||
import core_service.modules.library.repo.GameSpeedrunRepository;
|
import core_service.modules.library.repo.GameSpeedrunRepository;
|
||||||
|
import core_service.modules.library.repo.GameMoodsRepository;
|
||||||
|
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||||
import core_service.modules.library.repo.UserGameAchievementRepository;
|
import core_service.modules.library.repo.UserGameAchievementRepository;
|
||||||
import core_service.modules.library.repo.UserGameRepository;
|
import core_service.modules.library.repo.UserGameRepository;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamPlayerAchievementsResponse;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamSchemaForGameResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.web.client.ResourceAccessException;
|
import org.springframework.web.client.ResourceAccessException;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -37,7 +39,7 @@ import static org.mockito.Mockito.when;
|
|||||||
/**
|
/**
|
||||||
* The detail page refreshes stale rows in a transaction of its own, then reads them in its own read transaction.
|
* The detail page refreshes stale rows in a transaction of its own, then reads them in its own read transaction.
|
||||||
*/
|
*/
|
||||||
class GameDetailServiceTest extends AbstractIntegrationTest {
|
class GameDetailServiceTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 1L;
|
private static final long USER_ID = 1L;
|
||||||
private static final String STEAM_ID = "76561198000000001";
|
private static final String STEAM_ID = "76561198000000001";
|
||||||
@@ -63,17 +65,11 @@ class GameDetailServiceTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
||||||
|
|
||||||
@MockitoBean
|
@Autowired
|
||||||
private SteamApiService steamApiService;
|
private GameTimeToBeatRepository gameTimeToBeatRepository;
|
||||||
|
|
||||||
@MockitoBean
|
@Autowired
|
||||||
private TimeToBeatService timeToBeatService;
|
private GameMoodsRepository gameMoodsRepository;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private MoodService moodService;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private SpeedrunService speedrunService;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private GameSpeedrunRepository gameSpeedrunRepository;
|
private GameSpeedrunRepository gameSpeedrunRepository;
|
||||||
@@ -86,6 +82,8 @@ class GameDetailServiceTest extends AbstractIntegrationTest {
|
|||||||
userAchievementRepository.deleteAll();
|
userAchievementRepository.deleteAll();
|
||||||
userAchievementMetaRepository.deleteAll();
|
userAchievementMetaRepository.deleteAll();
|
||||||
gameSpeedrunRepository.deleteAll();
|
gameSpeedrunRepository.deleteAll();
|
||||||
|
gameTimeToBeatRepository.deleteAll();
|
||||||
|
gameMoodsRepository.deleteAll();
|
||||||
steamGameRepository.save(new SteamGame(620L, "Portal 2", null, Instant.now()));
|
steamGameRepository.save(new SteamGame(620L, "Portal 2", null, Instant.now()));
|
||||||
userGameRepository.save(new UserGame(USER_ID, 620L, 60, Instant.now(), Instant.now()));
|
userGameRepository.save(new UserGame(USER_ID, 620L, 60, Instant.now(), Instant.now()));
|
||||||
}
|
}
|
||||||
@@ -115,7 +113,7 @@ class GameDetailServiceTest extends AbstractIntegrationTest {
|
|||||||
|
|
||||||
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, STEAM_ID, 620L).orElseThrow();
|
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, STEAM_ID, 620L).orElseThrow();
|
||||||
|
|
||||||
org.mockito.Mockito.verifyNoInteractions(steamApiService, speedrunService, timeToBeatService, moodService);
|
org.mockito.Mockito.verifyNoInteractions(steamApiService, igdbApiService, speedrunApiService);
|
||||||
assertEquals(true, detail.hidden());
|
assertEquals(true, detail.hidden());
|
||||||
assertEquals(1, detail.achievementsTotal());
|
assertEquals(1, detail.achievementsTotal());
|
||||||
}
|
}
|
||||||
@@ -124,20 +122,26 @@ class GameDetailServiceTest extends AbstractIntegrationTest {
|
|||||||
void getGameDetail_GameAddedByHand_AsksForTheTimeToBeatAndMoodsAgain() {
|
void getGameDetail_GameAddedByHand_AsksForTheTimeToBeatAndMoodsAgain() {
|
||||||
userGameRepository.save(UserGame.manual(USER_ID, 620L, "PS5", GameStatus.PLAYING, 60, Instant.now()));
|
userGameRepository.save(UserGame.manual(USER_ID, 620L, "PS5", GameStatus.PLAYING, 60, Instant.now()));
|
||||||
|
|
||||||
|
when(igdbApiService.isEnabled()).thenReturn(true);
|
||||||
|
when(igdbApiService.getTimeToBeat(Set.of(620L))).thenReturn(Optional.of(Map.of()));
|
||||||
|
when(igdbApiService.getGameTags(Set.of(620L))).thenReturn(Optional.of(Map.of()));
|
||||||
|
|
||||||
gameDetailService.getGameDetail(USER_ID, null, 620L);
|
gameDetailService.getGameDetail(USER_ID, null, 620L);
|
||||||
|
|
||||||
org.mockito.Mockito.verify(timeToBeatService).refreshInBackground(List.of(620L));
|
assertTrue(gameTimeToBeatRepository.findById(620L).isPresent());
|
||||||
org.mockito.Mockito.verify(moodService).refreshInBackground(List.of(620L));
|
assertTrue(gameMoodsRepository.findById(620L).isPresent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getGameDetail_ShowsTheSpeedrunLinkAndAsksForIt() {
|
void getGameDetail_ShowsTheSpeedrunLinkAndAsksForIt() {
|
||||||
gameSpeedrunRepository.upsert(620L, "om1mw4d2", "https://www.speedrun.com/portal_2", Instant.now());
|
gameSpeedrunRepository.upsert(620L, "om1mw4d2", "https://www.speedrun.com/portal_2",
|
||||||
|
Instant.now().minus(Duration.ofDays(31)));
|
||||||
|
when(speedrunApiService.isEnabled()).thenReturn(true);
|
||||||
|
|
||||||
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, null, 620L).orElseThrow();
|
GameDetailDto detail = gameDetailService.getGameDetail(USER_ID, null, 620L).orElseThrow();
|
||||||
|
|
||||||
assertEquals("https://www.speedrun.com/portal_2", detail.speedrunUrl());
|
assertEquals("https://www.speedrun.com/portal_2", detail.speedrunUrl());
|
||||||
org.mockito.Mockito.verify(speedrunService).refreshInBackground(List.of(620L));
|
org.mockito.Mockito.verify(speedrunApiService).findGames("Portal 2");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+23
-6
@@ -1,16 +1,16 @@
|
|||||||
package core_service.modules.library.service;
|
package core_service.modules.library.service;
|
||||||
|
|
||||||
import core_service.modules.library.domain.UserSteamSync;
|
import core_service.modules.library.domain.UserSteamSync;
|
||||||
|
import core_service.modules.library.repo.GameSpeedrunRepository;
|
||||||
|
import core_service.modules.speedrun_adapter.service.SpeedrunApiService;
|
||||||
import core_service.modules.library.repo.UserSteamSyncRepository;
|
import core_service.modules.library.repo.UserSteamSyncRepository;
|
||||||
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
import core_service.modules.steam_adapter.api.dto.response.SteamOwnedGamesResponse;
|
||||||
import core_service.modules.steam_adapter.service.SteamApiService;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -27,7 +27,7 @@ import static org.mockito.Mockito.when;
|
|||||||
/**
|
/**
|
||||||
* The claim and the finish of a sync are committed on their own, so other requests see RUNNING and FAILED.
|
* The claim and the finish of a sync are committed on their own, so other requests see RUNNING and FAILED.
|
||||||
*/
|
*/
|
||||||
class LibrarySyncWorkerConcurrencyTest extends AbstractIntegrationTest {
|
class LibrarySyncWorkerConcurrencyTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
private static final long USER_ID = 4711L;
|
private static final long USER_ID = 4711L;
|
||||||
private static final long APPID = 991220L;
|
private static final long APPID = 991220L;
|
||||||
@@ -42,8 +42,8 @@ class LibrarySyncWorkerConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private JdbcTemplate jdbcTemplate;
|
private JdbcTemplate jdbcTemplate;
|
||||||
|
|
||||||
@MockitoBean
|
@Autowired
|
||||||
private SteamApiService steamApiService;
|
private GameSpeedrunRepository gameSpeedrunRepository;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -53,6 +53,23 @@ class LibrarySyncWorkerConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
jdbcTemplate.update("DELETE FROM wishlist_game WHERE user_id = ?", USER_ID);
|
jdbcTemplate.update("DELETE FROM wishlist_game WHERE user_id = ?", USER_ID);
|
||||||
jdbcTemplate.update("DELETE FROM user_steam_sync WHERE user_id = ?", USER_ID);
|
jdbcTemplate.update("DELETE FROM user_steam_sync WHERE user_id = ?", USER_ID);
|
||||||
jdbcTemplate.update("DELETE FROM steam_game WHERE appid = ?", APPID);
|
jdbcTemplate.update("DELETE FROM steam_game WHERE appid = ?", APPID);
|
||||||
|
jdbcTemplate.update("DELETE FROM game_speedrun WHERE appid = ?", APPID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runSyncNow_TheFollowUpStoresTheSpeedrunRow() {
|
||||||
|
when(steamApiService.getWishlist(STEAM_ID)).thenReturn(Optional.empty());
|
||||||
|
when(steamApiService.getOwnedGames(STEAM_ID))
|
||||||
|
.thenReturn(List.of(new SteamOwnedGamesResponse.Game((int) APPID, "Half-Life 2", 0, "icon", true, 0)));
|
||||||
|
when(speedrunApiService.isEnabled()).thenReturn(true);
|
||||||
|
when(speedrunApiService.findGames("Half-Life 2")).thenReturn(Optional.of(List.of(
|
||||||
|
new SpeedrunApiService.Game("hl2", "Half-Life 2", 2004, "https://www.speedrun.com/hl2"))));
|
||||||
|
when(speedrunApiService.hasVerifiedRuns("hl2")).thenReturn(Optional.of(true));
|
||||||
|
|
||||||
|
worker.runSyncNow(USER_ID, STEAM_ID);
|
||||||
|
|
||||||
|
verify(speedrunApiService).hasVerifiedRuns("hl2");
|
||||||
|
assertThat(gameSpeedrunRepository.findById(APPID)).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+8
-15
@@ -2,12 +2,12 @@ package core_service.modules.library.service;
|
|||||||
|
|
||||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||||
import core_service.shared.test.AbstractIntegrationTest;
|
import core_service.shared.test.AbstractMockedAdaptersTest;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
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.test.context.bean.override.mockito.MockitoBean;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.transaction.support.TransactionTemplate;
|
||||||
|
|
||||||
@@ -26,7 +26,10 @@ import static org.mockito.Mockito.when;
|
|||||||
/**
|
/**
|
||||||
* Two users' syncs run in parallel and can miss the same app, e.g. every app right after the first deploy.
|
* Two users' syncs run in parallel and can miss the same app, e.g. every app right after the first deploy.
|
||||||
*/
|
*/
|
||||||
class TimeToBeatServiceConcurrencyTest extends AbstractIntegrationTest {
|
class TimeToBeatServiceConcurrencyTest extends AbstractMockedAdaptersTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JdbcTemplate jdbc;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TimeToBeatService timeToBeatService;
|
private TimeToBeatService timeToBeatService;
|
||||||
@@ -37,9 +40,6 @@ class TimeToBeatServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private PlatformTransactionManager transactionManager;
|
private PlatformTransactionManager transactionManager;
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
private IgdbApiService igdbApiService;
|
|
||||||
|
|
||||||
// Before as well: other integration tests leave rows in this shared table, and a fresh row would hide the race.
|
// Before as well: other integration tests leave rows in this shared table, and a fresh row would hide the race.
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -60,8 +60,8 @@ class TimeToBeatServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
timeToBeatService.refresh(List.of(620L));
|
timeToBeatService.refresh(List.of(620L));
|
||||||
firstWrote.countDown();
|
firstWrote.countDown();
|
||||||
await(secondStarted);
|
await(secondStarted);
|
||||||
// Keep the transaction open while the second sync writes the same row.
|
// Keep the transaction open until the second sync waits for the same row.
|
||||||
sleep();
|
awaitLockWaiter(jdbc);
|
||||||
}));
|
}));
|
||||||
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
|
CompletableFuture<Void> second = CompletableFuture.runAsync(() -> {
|
||||||
await(firstWrote);
|
await(firstWrote);
|
||||||
@@ -82,11 +82,4 @@ class TimeToBeatServiceConcurrencyTest extends AbstractIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void sleep() {
|
|
||||||
try {
|
|
||||||
Thread.sleep(500);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,24 @@
|
|||||||
package core_service.shared.test;
|
package core_service.shared.test;
|
||||||
|
|
||||||
|
import core_service.shared.background.BackgroundPools;
|
||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||||
|
import org.springframework.core.task.SyncTaskExecutor;
|
||||||
|
import org.springframework.core.task.TaskExecutor;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||||
import org.springframework.test.context.DynamicPropertySource;
|
import org.springframework.test.context.DynamicPropertySource;
|
||||||
|
import org.springframework.test.context.bean.override.convention.TestBean;
|
||||||
import org.testcontainers.containers.PostgreSQLContainer;
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
|
|
||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for integration tests.
|
* Base class for integration tests.
|
||||||
@@ -37,6 +44,63 @@ public abstract class AbstractIntegrationTest {
|
|||||||
postgres.start();
|
postgres.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Background work is done when the call returns, so a test reads the result at once. The default pool runs it on
|
||||||
|
// the calling thread. The follow-up pool runs it on a new thread and waits: an after-commit listener then has its
|
||||||
|
// own transactions, as in production, and does not join the finished one of the sync.
|
||||||
|
@TestBean(name = BackgroundPools.DEFAULT)
|
||||||
|
private TaskExecutor applicationTaskExecutor;
|
||||||
|
|
||||||
|
@TestBean(name = BackgroundPools.SYNC_FOLLOW_UP)
|
||||||
|
private TaskExecutor syncFollowUpExecutor;
|
||||||
|
|
||||||
|
static TaskExecutor applicationTaskExecutor() {
|
||||||
|
return new SyncTaskExecutor();
|
||||||
|
}
|
||||||
|
|
||||||
|
static TaskExecutor syncFollowUpExecutor() {
|
||||||
|
return task -> {
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Thread thread = Thread.ofVirtual().start(() -> {
|
||||||
|
try {
|
||||||
|
task.run();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
failure.set(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
if (!thread.join(Duration.ofSeconds(30))) {
|
||||||
|
throw new AssertionError("Follow-up task still runs after 30 s");
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
if (failure.get() != null) {
|
||||||
|
throw new AssertionError("Follow-up task failed", failure.get());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until another connection waits for a row lock: the race a concurrency test sets up is then in place.
|
||||||
|
*/
|
||||||
|
protected static void awaitLockWaiter(JdbcTemplate jdbc) {
|
||||||
|
for (int attempt = 0; attempt < 250; attempt++) {
|
||||||
|
Integer waiting = jdbc.queryForObject(
|
||||||
|
"select count(*) from pg_stat_activity where wait_event_type = 'Lock'", Integer.class);
|
||||||
|
if (waiting != null && waiting > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(20);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new AssertionError("No connection waits for a lock");
|
||||||
|
}
|
||||||
|
|
||||||
// No key is committed: a fresh one per JVM. Tests that sign tokens use TEST_KEYS.getPrivate().
|
// No key is committed: a fresh one per JVM. Tests that sign tokens use TEST_KEYS.getPrivate().
|
||||||
protected static final KeyPair TEST_KEYS = generateKeys();
|
protected static final KeyPair TEST_KEYS = generateKeys();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package core_service.shared.test;
|
||||||
|
|
||||||
|
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||||
|
import core_service.modules.speedrun_adapter.service.SpeedrunApiService;
|
||||||
|
import core_service.modules.steam_adapter.service.SteamApiService;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests with the three external APIs mocked, in one shared context. A mock answers empty, so a sync a
|
||||||
|
* request starts finds no games and leaves the library as it is.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractMockedAdaptersTest extends AbstractIntegrationTest {
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected SteamApiService steamApiService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected IgdbApiService igdbApiService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected SpeedrunApiService speedrunApiService;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package core_service.shared.test;
|
||||||
|
|
||||||
|
import core_service.modules.library.service.LibrarySyncService;
|
||||||
|
import core_service.modules.library.service.MoodService;
|
||||||
|
import core_service.modules.library.service.SpeedrunService;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests of the picker and stats pages: the sync and the background refreshes they start are mocked.
|
||||||
|
*/
|
||||||
|
public abstract class AbstractMockedLibraryServicesTest extends AbstractIntegrationTest {
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected LibrarySyncService syncService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected MoodService moodService;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
protected SpeedrunService speedrunService;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user