Public Access
refactor(api): one error body, token audiences, contract fixture
This commit is contained in:
1 parent
2360beeb81
commit
73cfcb58e2
24 files changed
+633
-129
No files matched your search
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.lenient;
|
||||
*/
|
||||
class AccessTokenFiltersTest extends AbstractUnitTest {
|
||||
|
||||
private static final UserPrincipal USER = new UserPrincipal("42", "a@b.de", 76561198000000042L);
|
||||
private static final UserPrincipal USER = new UserPrincipal("42", 76561198000000042L);
|
||||
|
||||
@Mock
|
||||
private JwtService jwtService;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package core_service.configuration.security;
|
||||
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@@ -25,13 +24,7 @@ class ExpiredAccessTokenTest extends AbstractIntegrationTest {
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private static String expiredAccessToken() {
|
||||
return Jwts.builder()
|
||||
.subject("a@b.de")
|
||||
.claim("userId", "42")
|
||||
.issuedAt(new Date(System.currentTimeMillis() - 120_000))
|
||||
.expiration(new Date(System.currentTimeMillis() - 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
return ContractTokens.sign(ContractTokens.login(42, Role.USER, null), TEST_KEYS.getPrivate(), -60_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -4,6 +4,7 @@ import core_service.shared.security.ProfileTicket;
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.security.UserPrincipal;
|
||||
import core_service.shared.test.AbstractUnitTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -16,6 +17,7 @@ import java.security.PrivateKey;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -62,7 +64,7 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
void parseLoginToken_ReturnsTheUser() {
|
||||
String token = signToken("0815", "test@example.com", 123456789L);
|
||||
|
||||
assertEquals(Optional.of(new UserPrincipal("0815", "test@example.com", 123456789L)),
|
||||
assertEquals(Optional.of(new UserPrincipal("0815", 123456789L)),
|
||||
jwtService.parseLoginToken(token));
|
||||
}
|
||||
|
||||
@@ -205,4 +207,66 @@ class JwtServiceTest extends AbstractUnitTest {
|
||||
|
||||
assertEquals(Role.USER, jwtService.parseLoginToken(token).orElseThrow().role());
|
||||
}
|
||||
|
||||
private String sign(Map<String, Object> claims) {
|
||||
return ContractTokens.sign(claims, signingKey, 60_000);
|
||||
}
|
||||
|
||||
// The shapes of item 29: without the claims an Auth from before item 26 needs.
|
||||
private static Map<String, Object> without(Map<String, Object> claims, String... names) {
|
||||
for (String name : names) {
|
||||
claims.remove(name);
|
||||
}
|
||||
return claims;
|
||||
}
|
||||
|
||||
@Test
|
||||
void contractTokens_OfEveryKind_AreAccepted() {
|
||||
assertEquals("42", jwtService.parseLoginToken(sign(ContractTokens.claims(ContractTokens.LOGIN))).orElseThrow().id());
|
||||
assertEquals(42L, jwtService.verifyProfileTicket(sign(ContractTokens.claims(ContractTokens.PUBLIC_PROFILE)))
|
||||
.orElseThrow().userId());
|
||||
assertTrue(jwtService.isServiceToken(sign(ContractTokens.claims(ContractTokens.SERVICE))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void legacyTokens_OfAnAuthFromBeforeItem26_AreAccepted() {
|
||||
assertEquals("42", jwtService.parseLoginToken(sign(ContractTokens.legacyClaims(ContractTokens.LOGIN))).orElseThrow().id());
|
||||
assertEquals(42L, jwtService.verifyProfileTicket(sign(ContractTokens.legacyClaims(ContractTokens.PUBLIC_PROFILE)))
|
||||
.orElseThrow().userId());
|
||||
assertTrue(jwtService.isServiceToken(sign(ContractTokens.legacyClaims(ContractTokens.SERVICE))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void tokensWithoutTheOldClaims_AreAccepted() {
|
||||
assertEquals("42", jwtService.parseLoginToken(sign(without(ContractTokens.claims(ContractTokens.LOGIN), "userId")))
|
||||
.orElseThrow().id());
|
||||
assertEquals(42L, jwtService.verifyProfileTicket(sign(without(ContractTokens.claims(ContractTokens.PUBLIC_PROFILE), "typ")))
|
||||
.orElseThrow().userId());
|
||||
assertTrue(jwtService.isServiceToken(sign(without(ContractTokens.claims(ContractTokens.SERVICE), "typ"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aTokenForAnotherAudience_IsRejected() {
|
||||
Map<String, Object> ticketAsLogin = ContractTokens.claims(ContractTokens.LOGIN);
|
||||
ticketAsLogin.put("aud", List.of(JwtService.PROFILE_TICKET_AUDIENCE));
|
||||
Map<String, Object> loginAsService = ContractTokens.claims(ContractTokens.SERVICE);
|
||||
loginAsService.put("aud", List.of(JwtService.LOGIN_AUDIENCE));
|
||||
Map<String, Object> loginAsTicket = ContractTokens.claims(ContractTokens.PUBLIC_PROFILE);
|
||||
loginAsTicket.put("aud", List.of(JwtService.LOGIN_AUDIENCE));
|
||||
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(sign(ticketAsLogin)));
|
||||
assertFalse(jwtService.isServiceToken(sign(loginAsService)));
|
||||
assertEquals(Optional.empty(), jwtService.verifyProfileTicket(sign(loginAsTicket)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aTokenWithAnAudienceButAnotherIssuer_IsRejected() {
|
||||
Map<String, Object> login = ContractTokens.claims(ContractTokens.LOGIN);
|
||||
login.put("iss", "someone-else");
|
||||
Map<String, Object> service = ContractTokens.claims(ContractTokens.SERVICE);
|
||||
service.remove("iss");
|
||||
|
||||
assertEquals(Optional.empty(), jwtService.parseLoginToken(sign(login)));
|
||||
assertFalse(jwtService.isServiceToken(sign(service)));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package core_service.configuration.security;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
@@ -19,8 +19,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -77,9 +77,10 @@ class PublicEndpointsTest {
|
||||
|
||||
@Test
|
||||
void profileTicketIsNoLogin() throws Exception {
|
||||
String ticket = Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", "1")
|
||||
.claim("userId", "1").expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
Map<String, Object> claims = ContractTokens.ticket(1, null, null);
|
||||
// A ticket that also carries a user id is still no login.
|
||||
claims.put("userId", "1");
|
||||
String ticket = ContractTokens.sign(claims, KEYS.getPrivate(), 60_000);
|
||||
|
||||
int status = mockMvc.perform(get("/api/library/games-count").header("Authorization", "Bearer " + ticket))
|
||||
.andReturn().getResponse().getStatus();
|
||||
@@ -95,9 +96,8 @@ class PublicEndpointsTest {
|
||||
// Without a login: a class role marker on the way would answer 401.
|
||||
@Test
|
||||
void publicProfileWithSharedStats_WorksWithoutALogin() throws Exception {
|
||||
String ticket = Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", "1")
|
||||
.claim("profileStats", List.of("TOP_GAMES", "OVERVIEW")).expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
String ticket = ContractTokens.sign(ContractTokens.ticket(1, List.of("TOP_GAMES", "OVERVIEW"), null),
|
||||
KEYS.getPrivate(), 60_000);
|
||||
|
||||
mockMvc.perform(get("/api/public/profiles/library").header("X-Profile-Ticket", ticket))
|
||||
.andExpect(status().isOk())
|
||||
|
||||
@@ -4,7 +4,7 @@ import core_service.shared.security.Public;
|
||||
import core_service.shared.security.RequiresRole;
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -18,8 +18,6 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -93,13 +91,7 @@ class RoleCheckFlowTest extends AbstractIntegrationTest {
|
||||
private ProbeService probeService;
|
||||
|
||||
private static String token(Role role) {
|
||||
return Jwts.builder()
|
||||
.subject("probe@example.com")
|
||||
.claim("userId", "4711")
|
||||
.claim("role", role.name())
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
return ContractTokens.loginToken(4711, role);
|
||||
}
|
||||
|
||||
private ResultActions call(String path, Role role) throws Exception {
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
package core_service.modules.account.api;
|
||||
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.JwtBuilder;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@@ -40,20 +44,15 @@ class InternalAccountControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
jdbc.update("delete from deleted_user where user_id = ?", USER_ID);
|
||||
}
|
||||
|
||||
private static String token(JwtBuilder builder) {
|
||||
return builder.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
jdbc.update("delete from deleted_user where user_id in (?, 42)", USER_ID);
|
||||
}
|
||||
|
||||
private static String serviceToken() {
|
||||
return token(Jwts.builder().subject("tbp-auth").claim("typ", "service"));
|
||||
return ContractTokens.serviceToken();
|
||||
}
|
||||
|
||||
private static String loginToken() {
|
||||
return token(Jwts.builder().subject("a@b.de").claim("userId", String.valueOf(USER_ID)));
|
||||
return ContractTokens.loginToken(USER_ID);
|
||||
}
|
||||
|
||||
private ResultActions send(MockHttpServletRequestBuilder request, String body, String token) throws Exception {
|
||||
@@ -107,6 +106,38 @@ class InternalAccountControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
@Test
|
||||
void prune_WithAnEmptyListIs400() throws Exception {
|
||||
send(post("/internal/accounts/prune"), "{\"knownUserIds\":[],\"dryRun\":false}", serviceToken()).andExpect(status().isBadRequest());
|
||||
send(post("/internal/accounts/prune"), "{\"knownUserIds\":[],\"dryRun\":false}", serviceToken())
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.fields[0].field").value("knownUserIds"));
|
||||
}
|
||||
|
||||
private static MockHttpServletRequestBuilder contractRequest(JsonNode call) {
|
||||
return request(HttpMethod.valueOf(call.path("method").asString()), call.path("path").asString());
|
||||
}
|
||||
|
||||
// The calls as Auth's CoreAccountService sends them, with the service tokens of an old and a new Auth.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
void contractCalls_AreAnswered(boolean legacyToken) throws Exception {
|
||||
String token = legacyToken ? ContractTokens.sign(ContractTokens.legacyClaims(ContractTokens.SERVICE))
|
||||
: ContractTokens.serviceToken();
|
||||
// replaceDeleted with [] removes every tombstone, also those of other tests in this database.
|
||||
List<Long> tombstones = jdbc.queryForList("select user_id from deleted_user", Long.class);
|
||||
try {
|
||||
for (String name : List.of("markDeleted", "replaceDeleted")) {
|
||||
JsonNode call = ContractTokens.internalCall(name);
|
||||
send(contractRequest(call), call.path("request").toString(), token).andExpect(status().isNoContent());
|
||||
}
|
||||
} finally {
|
||||
tombstones.forEach(userId -> jdbc.update("insert into deleted_user (user_id) values (?) on conflict do nothing", userId));
|
||||
}
|
||||
for (String name : List.of("purge", "prune")) {
|
||||
JsonNode call = ContractTokens.internalCall(name);
|
||||
ResultActions result = send(contractRequest(call), call.path("request").toString(), token).andExpect(status().isOk());
|
||||
for (String field : call.path("response").propertyNames()) {
|
||||
result.andExpect(jsonPath("$." + field).exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -22,7 +22,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
@@ -77,13 +76,7 @@ class GameCorrectionFlowTest extends AbstractIntegrationTest {
|
||||
}
|
||||
|
||||
private static String token(Role role) {
|
||||
return Jwts.builder()
|
||||
.subject("auditor@example.com")
|
||||
.claim("userId", String.valueOf(AUDITOR_ID))
|
||||
.claim("role", role.name())
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
return ContractTokens.loginToken(AUDITOR_ID, role);
|
||||
}
|
||||
|
||||
private ResultActions correct(Role role, long appid, String field, String valueJson) throws Exception {
|
||||
|
||||
@@ -7,16 +7,16 @@ import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
import core_service.modules.library.domain.UserStats;
|
||||
import core_service.modules.library.repo.GameCorrectionRepository;
|
||||
import core_service.modules.library.repo.GameMoodsRepository;
|
||||
import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.modules.library.repo.UserStatsRepository;
|
||||
import core_service.modules.library.domain.UserStats;
|
||||
import core_service.shared.security.Role;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -30,7 +30,6 @@ import org.springframework.test.web.servlet.ResultActions;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -94,13 +93,7 @@ class GameMergeFlowTest extends AbstractIntegrationTest {
|
||||
}
|
||||
|
||||
private static String token(Role role) {
|
||||
return Jwts.builder()
|
||||
.subject("auditor@example.com")
|
||||
.claim("userId", String.valueOf(AUDITOR_ID))
|
||||
.claim("role", role.name())
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
return ContractTokens.loginToken(AUDITOR_ID, role);
|
||||
}
|
||||
|
||||
private ResultActions merge(Role role, long appid, long otherAppid) throws Exception {
|
||||
|
||||
@@ -14,17 +14,28 @@ import core_service.shared.test.AbstractIntegrationTest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
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 java.sql.SQLException;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@@ -102,6 +113,50 @@ class LibraryControllerStatsTest extends AbstractIntegrationTest {
|
||||
@Test
|
||||
void stats_UnknownGroup_Is400() throws Exception {
|
||||
mockMvc.perform(get("/api/library/stats").param("groups", "MONEY"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.fields[0].field").value("groups"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anUnexpectedError_Is500WithoutDetails() throws Exception {
|
||||
doThrow(new IllegalStateException("secret detail")).when(moodService).refreshListedInBackground(USER_ID);
|
||||
|
||||
mockMvc.perform(get("/api/library/stats"))
|
||||
.andExpect(status().isInternalServerError())
|
||||
.andExpect(jsonPath("$.code").value("INTERNAL_ERROR"))
|
||||
.andExpect(content().string(not(containsString("secret detail"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aDatabaseConflict_Is409() throws Exception {
|
||||
doThrow(new DataIntegrityViolationException("doppelter Schlüsselwert", new SQLException("doppelter Schlüsselwert", "23505"))).when(moodService).refreshListedInBackground(USER_ID);
|
||||
|
||||
mockMvc.perform(get("/api/library/stats"))
|
||||
.andExpect(status().isConflict())
|
||||
.andExpect(jsonPath("$.code").value("CONFLICT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anotherRefusedWrite_Is500() throws Exception {
|
||||
doThrow(new DataIntegrityViolationException("Wert zu lang", new SQLException("Wert zu lang", "22001")))
|
||||
.when(moodService).refreshListedInBackground(USER_ID);
|
||||
|
||||
mockMvc.perform(get("/api/library/stats"))
|
||||
.andExpect(status().isInternalServerError())
|
||||
.andExpect(jsonPath("$.code").value("INTERNAL_ERROR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
void aSpringServerError_IsLoggedAsInternalError(CapturedOutput output) throws Exception {
|
||||
doThrow(new HttpMessageNotWritableException("body not writable"))
|
||||
.when(moodService).refreshListedInBackground(USER_ID);
|
||||
|
||||
mockMvc.perform(get("/api/library/stats"))
|
||||
.andExpect(status().isInternalServerError())
|
||||
.andExpect(jsonPath("$.code").value("INTERNAL_ERROR"));
|
||||
|
||||
assertTrue(output.getAll().contains("body not writable"), "the 500 left no trace in the log");
|
||||
}
|
||||
}
|
||||
+2
-8
@@ -2,7 +2,7 @@ package core_service.modules.library.api;
|
||||
|
||||
import core_service.modules.library.repo.UserSteamSyncRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -13,7 +13,6 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
@@ -42,12 +41,7 @@ class LibraryControllerSteamIdGoneTest extends AbstractIntegrationTest {
|
||||
}
|
||||
|
||||
private static Cookie loginWithoutSteamId() {
|
||||
return new Cookie("tbp_access", Jwts.builder()
|
||||
.subject("unlinked@example.com")
|
||||
.claim("userId", String.valueOf(USER_ID))
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA)
|
||||
.compact());
|
||||
return new Cookie("tbp_access", ContractTokens.loginToken(USER_ID));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -3,8 +3,8 @@ package core_service.modules.library.api;
|
||||
import core_service.configuration.security.ClientHeaderFilter;
|
||||
import core_service.modules.igdb_adapter.service.IgdbApiService;
|
||||
import core_service.modules.library.domain.GameSource;
|
||||
import core_service.modules.library.domain.GameTimeToBeat;
|
||||
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.UserGame;
|
||||
import core_service.modules.library.domain.UserGameId;
|
||||
@@ -12,6 +12,7 @@ import core_service.modules.library.repo.GameTimeToBeatRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -215,6 +216,14 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
|
||||
add(119388, " ").andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_OneLetter_Is400WithTheField() throws Exception {
|
||||
mockMvc.perform(get("/api/library/game-search").param("q", "a"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"))
|
||||
.andExpect(jsonPath("$.fields[0].field").value("q"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void search_MarksGamesAlreadyInTheLibrary() throws Exception {
|
||||
steamGameRepository.save(new SteamGame(1245620L, "ELDEN RING", null, Instant.now()));
|
||||
@@ -342,12 +351,7 @@ class ManualGameControllerTest extends AbstractIntegrationTest {
|
||||
|
||||
// Signed like an Auth login token, for a user who saved no Steam ID.
|
||||
private static String tokenWithoutSteamId() {
|
||||
return io.jsonwebtoken.Jwts.builder()
|
||||
.subject("no-steam@example.com")
|
||||
.claim("userId", String.valueOf(USER_ID))
|
||||
.expiration(new java.util.Date(System.currentTimeMillis() + 60_000))
|
||||
.signWith(TEST_KEYS.getPrivate(), io.jsonwebtoken.Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
return ContractTokens.loginToken(USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -11,7 +11,7 @@ import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -19,7 +19,6 @@ import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
@@ -49,20 +48,15 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
private UserGameAchievementMetaRepository userAchievementMetaRepository;
|
||||
|
||||
private static String ticket(long userId) {
|
||||
return Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", String.valueOf(userId))
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
return ContractTokens.ticketToken(userId, null, null);
|
||||
}
|
||||
|
||||
private static String ticket(long userId, List<String> profileStats) {
|
||||
return Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", String.valueOf(userId))
|
||||
.claim("profileStats", profileStats)
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
return ContractTokens.ticketToken(userId, profileStats, null);
|
||||
}
|
||||
|
||||
private static String ticket(long userId, List<String> profileStats, List<String> sections) {
|
||||
return Jwts.builder().subject("public-profile").claim("typ", "public-profile").claim("profileUserId", String.valueOf(userId))
|
||||
.claim("profileStats", profileStats).claim("sections", sections)
|
||||
.expiration(new Date(System.currentTimeMillis() + 60_000)).signWith(TEST_KEYS.getPrivate(), Jwts.SIG.EdDSA).compact();
|
||||
return ContractTokens.ticketToken(userId, profileStats, sections);
|
||||
}
|
||||
|
||||
private UserGame game(long appid, int playtime, GameStatus status, Integer rating) {
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package core_service.shared.test;
|
||||
|
||||
import core_service.shared.security.Role;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Tokens only in the shapes of {@code contract/auth-core-contract.json}, the file Auth checks its tokens against. A
|
||||
* test changes the values, never the claim names.
|
||||
*/
|
||||
public final class ContractTokens {
|
||||
|
||||
public static final String LOGIN = "login";
|
||||
public static final String PUBLIC_PROFILE = "publicProfile";
|
||||
public static final String SERVICE = "service";
|
||||
|
||||
private static final JsonMapper MAPPER = JsonMapper.builder().build();
|
||||
private static final JsonNode CONTRACT = load();
|
||||
|
||||
private ContractTokens() {
|
||||
}
|
||||
|
||||
private static JsonNode load() {
|
||||
try (InputStream in = ContractTokens.class.getResourceAsStream("/contract/auth-core-contract.json")) {
|
||||
return MAPPER.readTree(in);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The claims Auth signs for this kind today.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> claims(String kind) {
|
||||
return new LinkedHashMap<>(MAPPER.treeToValue(CONTRACT.path("tokens").required(kind), Map.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* The claims of an Auth from before item 26.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> legacyClaims(String kind) {
|
||||
return new LinkedHashMap<>(MAPPER.treeToValue(CONTRACT.path("legacyTokens").required(kind), Map.class));
|
||||
}
|
||||
|
||||
public static JsonNode internalCall(String name) {
|
||||
return CONTRACT.path("internal").required(name);
|
||||
}
|
||||
|
||||
public static String sign(Map<String, Object> claims, PrivateKey key, long expiresInMs) {
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.expiration(new Date(System.currentTimeMillis() + expiresInMs))
|
||||
.signWith(key, Jwts.SIG.EdDSA)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public static String sign(Map<String, Object> claims) {
|
||||
return sign(claims, AbstractIntegrationTest.TEST_KEYS.getPrivate(), 60_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param steamId null for a user without a linked Steam account
|
||||
*/
|
||||
public static Map<String, Object> login(long userId, Role role, Long steamId) {
|
||||
Map<String, Object> claims = claims(LOGIN);
|
||||
claims.put("sub", String.valueOf(userId));
|
||||
claims.put("userId", String.valueOf(userId));
|
||||
claims.put("role", role.name());
|
||||
putOrRemove(claims, "steamId", steamId);
|
||||
return claims;
|
||||
}
|
||||
|
||||
public static String loginToken(long userId, Role role) {
|
||||
return sign(login(userId, role, null));
|
||||
}
|
||||
|
||||
public static String loginToken(long userId) {
|
||||
return loginToken(userId, Role.USER);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param profileStats null for a ticket without the claim
|
||||
* @param sections null for a ticket without the claim
|
||||
*/
|
||||
public static Map<String, Object> ticket(long userId, List<String> profileStats, List<String> sections) {
|
||||
Map<String, Object> claims = claims(PUBLIC_PROFILE);
|
||||
claims.put("profileUserId", String.valueOf(userId));
|
||||
putOrRemove(claims, "profileStats", profileStats);
|
||||
putOrRemove(claims, "sections", sections);
|
||||
return claims;
|
||||
}
|
||||
|
||||
public static String ticketToken(long userId, List<String> profileStats, List<String> sections) {
|
||||
return sign(ticket(userId, profileStats, sections));
|
||||
}
|
||||
|
||||
public static String serviceToken() {
|
||||
return sign(claims(SERVICE));
|
||||
}
|
||||
|
||||
private static void putOrRemove(Map<String, Object> claims, String name, Object value) {
|
||||
if (value == null) {
|
||||
claims.remove(name);
|
||||
} else {
|
||||
claims.put(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"_about": "The token and internal-call contract between Auth and Core. The same file is in both repos; ToBePlayed-Deployment/scripts/check-contract.sh compares them. iat and exp are left out. legacyTokens are the shapes of an Auth from before item 26: Core and Auth accept them until item 29.",
|
||||
"tokens": {
|
||||
"login": {
|
||||
"iss": "tbp-auth",
|
||||
"aud": ["tbp-login"],
|
||||
"sub": "42",
|
||||
"userId": "42",
|
||||
"role": "USER",
|
||||
"steamId": 76561198000000001
|
||||
},
|
||||
"publicProfile": {
|
||||
"iss": "tbp-auth",
|
||||
"aud": ["tbp-public-profile"],
|
||||
"sub": "public-profile",
|
||||
"typ": "public-profile",
|
||||
"profileUserId": "42",
|
||||
"profileStats": ["OVERVIEW"],
|
||||
"sections": ["SUMMARY"]
|
||||
},
|
||||
"service": {
|
||||
"iss": "tbp-auth",
|
||||
"aud": ["tbp-internal"],
|
||||
"sub": "tbp-auth",
|
||||
"typ": "service"
|
||||
}
|
||||
},
|
||||
"legacyTokens": {
|
||||
"login": {
|
||||
"sub": "user@example.com",
|
||||
"userId": "42",
|
||||
"role": "USER",
|
||||
"steamId": 76561198000000001
|
||||
},
|
||||
"publicProfile": {
|
||||
"sub": "public-profile",
|
||||
"typ": "public-profile",
|
||||
"profileUserId": "42",
|
||||
"profileStats": ["OVERVIEW"],
|
||||
"sections": ["SUMMARY"]
|
||||
},
|
||||
"service": {
|
||||
"sub": "tbp-auth",
|
||||
"typ": "service"
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"markDeleted": {
|
||||
"method": "POST",
|
||||
"path": "/internal/accounts/deleted",
|
||||
"request": {"userIds": [42]}
|
||||
},
|
||||
"replaceDeleted": {
|
||||
"method": "PUT",
|
||||
"path": "/internal/accounts/deleted",
|
||||
"request": {"userIds": []}
|
||||
},
|
||||
"purge": {
|
||||
"method": "POST",
|
||||
"path": "/internal/accounts/purge",
|
||||
"request": {"userIds": [42], "dryRun": false},
|
||||
"response": {"dryRun": true, "users": [42], "rows": {"user_game": 3}, "refused": false}
|
||||
},
|
||||
"prune": {
|
||||
"method": "POST",
|
||||
"path": "/internal/accounts/prune",
|
||||
"request": {"knownUserIds": [1, 42], "dryRun": true},
|
||||
"response": {"dryRun": true, "users": [42], "rows": {"user_game": 3}, "refused": false}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user