Add JWT-based security and /me endpoint
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Introduced JWT-based authentication with role handling using Keycloak. Added the `/me` endpoint to return user information and roles. Configured testing, Keycloak integration, and public-facing `/public/info` endpoint enhancements.
This commit is contained in:
@ -10,4 +10,5 @@ ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /workspace/build/libs/*-SNAPSHOT.jar app.jar
|
COPY --from=build /workspace/build/libs/*-SNAPSHOT.jar app.jar
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
LABEL com.centurylinklabs.watchtower.enable=true
|
||||||
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"]
|
ENTRYPOINT ["sh","-c","java $JAVA_OPTS -jar /app/app.jar"]
|
||||||
|
|||||||
@ -12,14 +12,19 @@ java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
|
|||||||
repositories { mavenCentral() }
|
repositories { mavenCentral() }
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
|
||||||
|
implementation 'org.springframework.security:spring-security-oauth2-jose'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
|
||||||
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
|
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
implementation 'org.flywaydb:flyway-core'
|
implementation 'org.flywaydb:flyway-core'
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||||
|
testImplementation 'org.springframework.security:spring-security-test'
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
|||||||
@ -16,12 +16,15 @@ services:
|
|||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:24.0
|
image: quay.io/keycloak/keycloak:24.0
|
||||||
command: ["start-dev","--http-port=8081"]
|
command: ["start-dev","--http-port=8081","--import-realm"]
|
||||||
environment:
|
environment:
|
||||||
KEYCLOAK_ADMIN: admin
|
KEYCLOAK_ADMIN: admin
|
||||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||||
|
volumes:
|
||||||
|
- ./keycloak:/opt/keycloak/data/import
|
||||||
ports: ["8081:8081"]
|
ports: ["8081:8081"]
|
||||||
|
|
||||||
|
|
||||||
api:
|
api:
|
||||||
build: .
|
build: .
|
||||||
image: registry.local:5000/hemhub/api:dev
|
image: registry.local:5000/hemhub/api:dev
|
||||||
|
|||||||
56
keycloak/realm-hemhub.json
Normal file
56
keycloak/realm-hemhub.json
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"realm": "hemhub",
|
||||||
|
"enabled": true,
|
||||||
|
"displayName": "HemHub",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"username": "maria",
|
||||||
|
"email": "maria@example.com",
|
||||||
|
"enabled": true,
|
||||||
|
"emailVerified": true,
|
||||||
|
"attributes": { "household_id": ["H-ANDERSSON"] },
|
||||||
|
"credentials": [{ "type": "password", "value": "Passw0rd!", "temporary": false }],
|
||||||
|
"realmRoles": ["OWNER","MEMBER"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "ulf",
|
||||||
|
"email": "ulf@example.com",
|
||||||
|
"enabled": true,
|
||||||
|
"emailVerified": true,
|
||||||
|
"attributes": { "household_id": ["H-ANDERSSON"] },
|
||||||
|
"credentials": [{ "type": "password", "value": "Passw0rd!", "temporary": false }],
|
||||||
|
"realmRoles": ["MEMBER"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": {
|
||||||
|
"realm": [
|
||||||
|
{"name":"OWNER","composite":false},
|
||||||
|
{"name":"MEMBER","composite":false},
|
||||||
|
{"name":"ADMIN","composite":false}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"clientId": "hemhub-public",
|
||||||
|
"publicClient": true,
|
||||||
|
"redirectUris": ["http://localhost:5173/*","http://localhost:8080/swagger-ui/*"],
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"attributes": { "pkce.code.challenge.method": "S256" }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clientId": "hemhub-service",
|
||||||
|
"serviceAccountsEnabled": true,
|
||||||
|
"secret": "dev-secret",
|
||||||
|
"publicClient": false,
|
||||||
|
"redirectUris": [],
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"standardFlowEnabled": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clientScopes": [
|
||||||
|
{"name":"roles","protocol":"openid-connect"}
|
||||||
|
],
|
||||||
|
"defaultDefaultClientScopes": ["roles", "profile", "email"]
|
||||||
|
}
|
||||||
@ -0,0 +1,60 @@
|
|||||||
|
package se.urmo.hemhub.config;
|
||||||
|
|
||||||
|
import org.springframework.core.convert.converter.Converter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class KeycloakRealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<GrantedAuthority> convert(Jwt jwt) {
|
||||||
|
// realm_access.roles (Keycloak)
|
||||||
|
var realmRoles = extractRoles(jwt.getClaim("realm_access"), "roles");
|
||||||
|
|
||||||
|
// (valfritt) resource_access.<client>.roles — plocka även klientroller om du vill
|
||||||
|
var resourceAccess = jwt.getClaim("resource_access");
|
||||||
|
var clientRoles = new ArrayList<String>();
|
||||||
|
if (resourceAccess instanceof Map<?,?> ra) {
|
||||||
|
ra.values().forEach(v -> clientRoles.addAll(extractRoles(v, "roles")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// scope/scp (standard i Spring): "read write" eller ["read","write"]
|
||||||
|
var scopeRoles = new ArrayList<String>();
|
||||||
|
Object scope = Optional.ofNullable(jwt.getClaims().get("scope"))
|
||||||
|
.orElse(jwt.getClaims().get("scp"));
|
||||||
|
if (scope instanceof String s) {
|
||||||
|
scopeRoles.addAll(Arrays.asList(s.split("\\s+")));
|
||||||
|
} else if (scope instanceof Collection<?> c) {
|
||||||
|
c.forEach(x -> scopeRoles.add(String.valueOf(x)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samla ihop och prefixa ROLE_
|
||||||
|
return Stream.of(realmRoles, clientRoles, scopeRoles)
|
||||||
|
.flatMap(Collection::stream)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(String::valueOf)
|
||||||
|
.filter(s -> !s.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.map(r -> r.startsWith("ROLE_") ? r : "ROLE_" + r)
|
||||||
|
.map(SimpleGrantedAuthority::new)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hjälpmetod som läser en lista av roller från en otypad struktur:
|
||||||
|
* förväntar sig t.ex. { "roles": ["OWNER","MEMBER"] }
|
||||||
|
*/
|
||||||
|
private static List<String> extractRoles(Object container, String key) {
|
||||||
|
if (!(container instanceof Map<?,?> m)) return List.of();
|
||||||
|
Object raw = m.get(key);
|
||||||
|
if (raw instanceof Collection<?> c) {
|
||||||
|
return c.stream().map(String::valueOf).toList();
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/java/se/urmo/hemhub/config/SecurityConfig.java
Normal file
45
src/main/java/se/urmo/hemhub/config/SecurityConfig.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package se.urmo.hemhub.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableMethodSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
.csrf(csrf -> csrf.disable())
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers(
|
||||||
|
"/public/**",
|
||||||
|
"/actuator/health", "/actuator/info",
|
||||||
|
"/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html"
|
||||||
|
).permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter())));
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
JwtAuthenticationConverter jwtConverter() {
|
||||||
|
var converter = new JwtAuthenticationConverter();
|
||||||
|
converter.setJwtGrantedAuthoritiesConverter(new KeycloakRealmRoleConverter());
|
||||||
|
return converter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
45
src/main/java/se/urmo/hemhub/web/MeController.java
Normal file
45
src/main/java/se/urmo/hemhub/web/MeController.java
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package se.urmo.hemhub.web;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class MeController {
|
||||||
|
|
||||||
|
@GetMapping("/me")
|
||||||
|
public Map<String, Object> me(Authentication auth) {
|
||||||
|
if (auth == null || !(auth.getPrincipal() instanceof Jwt jwt)) {
|
||||||
|
throw new IllegalStateException("No JWT principal");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Läs roller på ett säkert sätt ----
|
||||||
|
List<String> roles = extractRealmRoles(jwt);
|
||||||
|
|
||||||
|
// ---- Bygg svar ----
|
||||||
|
Map<String, Object> response = new LinkedHashMap<>();
|
||||||
|
response.put("sub", jwt.getSubject());
|
||||||
|
response.put("email", jwt.getClaimAsString("email"));
|
||||||
|
response.put("preferred_username", jwt.getClaimAsString("preferred_username"));
|
||||||
|
response.put("householdId", jwt.getClaimAsString("household_id"));
|
||||||
|
response.put("roles", roles);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> extractRealmRoles(Jwt jwt) {
|
||||||
|
Object realmAccess = jwt.getClaims().get("realm_access");
|
||||||
|
if (!(realmAccess instanceof Map<?,?> map)) return List.of();
|
||||||
|
|
||||||
|
Object rawRoles = map.get("roles");
|
||||||
|
if (rawRoles instanceof Collection<?> col) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
for (Object o : col) result.add(String.valueOf(o));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,14 +9,17 @@ import java.util.Map;
|
|||||||
@RestController
|
@RestController
|
||||||
public class PublicController {
|
public class PublicController {
|
||||||
|
|
||||||
@Value("${app.version:dev}")
|
private final String version;
|
||||||
String version;
|
|
||||||
|
public PublicController(@Value("${app.version:dev}") String version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/public/info")
|
@GetMapping("/public/info")
|
||||||
public Map<String, Object> info() {
|
public Map<String, Object> info() {
|
||||||
return Map.of(
|
return Map.of(
|
||||||
"name", "HemHub API",
|
"name", "HemHub API",
|
||||||
"version", version,
|
"version", version != null ? version : "dev",
|
||||||
"timestamp", Instant.now().toString()
|
"timestamp", Instant.now().toString()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,3 +11,8 @@ spring:
|
|||||||
ddl-auto: none
|
ddl-auto: none
|
||||||
flyway:
|
flyway:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
security:
|
||||||
|
oauth2:
|
||||||
|
resourceserver:
|
||||||
|
jwt:
|
||||||
|
issuer-uri: http://localhost:8082/realms/hemhub
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
package se.urmo.hemhub.config;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class KeycloakRealmRoleConverterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void picksRealmRoles_scopes_andClientRoles() {
|
||||||
|
var jwt = new Jwt(
|
||||||
|
"t", Instant.now(), Instant.now().plusSeconds(3600),
|
||||||
|
Map.of("alg","none"),
|
||||||
|
Map.of(
|
||||||
|
"realm_access", Map.of("roles", List.of("OWNER","MEMBER")),
|
||||||
|
"resource_access", Map.of(
|
||||||
|
"hemhub-public", Map.of("roles", List.of("reader"))
|
||||||
|
),
|
||||||
|
"scope", "read write"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
var conv = new KeycloakRealmRoleConverter();
|
||||||
|
var auths = conv.convert(jwt);
|
||||||
|
|
||||||
|
assertThat(auths).extracting("authority")
|
||||||
|
.contains("ROLE_OWNER","ROLE_MEMBER","ROLE_reader","ROLE_read","ROLE_write");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tolerantToMissingFields_returnsEmpty() {
|
||||||
|
var jwt = new Jwt("t", Instant.now(), Instant.now().plusSeconds(3600),
|
||||||
|
Map.of("alg","none"), Map.of("dummy", "value"));
|
||||||
|
var conv = new KeycloakRealmRoleConverter();
|
||||||
|
assertThat(conv.convert(jwt)).isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/test/java/se/urmo/hemhub/security/JwtTestConfig.java
Normal file
32
src/test/java/se/urmo/hemhub/security/JwtTestConfig.java
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package se.urmo.hemhub.security;
|
||||||
|
|
||||||
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.security.oauth2.jwt.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@TestConfiguration
|
||||||
|
public class JwtTestConfig {
|
||||||
|
@Bean
|
||||||
|
JwtDecoder jwtDecoder() {
|
||||||
|
return token -> {
|
||||||
|
// tokenformat i test: "HID:ROLE1,ROLE2"
|
||||||
|
String[] parts = token.split(":");
|
||||||
|
String hid = parts.length > 0 ? parts[0] : "H-TEST";
|
||||||
|
List<String> roles = List.of("MEMBER");
|
||||||
|
Instant now = Instant.now();
|
||||||
|
|
||||||
|
return new Jwt(token, now, now.plusSeconds(3600),
|
||||||
|
Map.of("alg","none"),
|
||||||
|
Map.of(
|
||||||
|
"sub","test-user",
|
||||||
|
"email","test@example.com",
|
||||||
|
"preferred_username","test",
|
||||||
|
"household_id", hid,
|
||||||
|
"realm_access", Map.of("roles", roles)
|
||||||
|
));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/test/java/se/urmo/hemhub/web/MeControllerBranchesIT.java
Normal file
47
src/test/java/se/urmo/hemhub/web/MeControllerBranchesIT.java
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package se.urmo.hemhub.web;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class MeControllerBranchesIT {
|
||||||
|
|
||||||
|
@Autowired MockMvc mvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void me_withoutRealmAccess_rolesEmpty() throws Exception {
|
||||||
|
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||||
|
j.subject("u1");
|
||||||
|
j.claim("preferred_username", "test1");
|
||||||
|
j.claim("household_id", "H1");
|
||||||
|
// ingen realm_access-claim
|
||||||
|
})))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.roles").isArray())
|
||||||
|
.andExpect(jsonPath("$.roles.length()").value(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void me_withMalformedRealmAccess_rolesEmpty() throws Exception {
|
||||||
|
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||||
|
j.subject("u2");
|
||||||
|
j.claim("preferred_username", "test2");
|
||||||
|
j.claim("household_id", "H2");
|
||||||
|
j.claim("realm_access", Map.of("roles", "NOT_A_LIST"));
|
||||||
|
})))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.roles.length()").value(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/test/java/se/urmo/hemhub/web/MeControllerIT.java
Normal file
39
src/test/java/se/urmo/hemhub/web/MeControllerIT.java
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package se.urmo.hemhub.integration;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class MeControllerIT {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void me_withToken_200_andContainsClaims() throws Exception {
|
||||||
|
mvc.perform(get("/me").with(jwt().jwt(j -> {
|
||||||
|
j.subject("test-user");
|
||||||
|
j.claim("email", "test@example.com");
|
||||||
|
j.claim("preferred_username", "test");
|
||||||
|
j.claim("household_id", "H-ANDERSSON");
|
||||||
|
j.claim("realm_access", Map.of("roles", List.of("MEMBER")));
|
||||||
|
})))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.preferred_username").value("test"))
|
||||||
|
.andExpect(jsonPath("$.householdId").value("H-ANDERSSON"))
|
||||||
|
.andExpect(jsonPath("$.roles[0]").value("MEMBER"));
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/test/java/se/urmo/hemhub/web/PublicControllerIT.java
Normal file
30
src/test/java/se/urmo/hemhub/web/PublicControllerIT.java
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package se.urmo.hemhub.web;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class PublicControllerIT {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void publicInfo_ok() throws Exception {
|
||||||
|
mvc.perform(get("/public/info"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.name").value("HemHub API"))
|
||||||
|
.andExpect(jsonPath("$.version").exists())
|
||||||
|
.andExpect(jsonPath("$.timestamp").exists());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
src/test/java/se/urmo/hemhub/web/PublicControllerTest.java
Normal file
17
src/test/java/se/urmo/hemhub/web/PublicControllerTest.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package se.urmo.hemhub.web;
|
||||||
|
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PublicControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void info_returnsExpectedKeys() {
|
||||||
|
var controller = new PublicController("test");
|
||||||
|
var result = controller.info();
|
||||||
|
assertThat(result).containsKeys("name", "version", "timestamp");
|
||||||
|
assertThat(result.get("name")).isEqualTo("HemHub API");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/test/resources/application-test.yml
Normal file
11
src/test/resources/application-test.yml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:mem:hemhub;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false
|
||||||
|
driverClassName: org.h2.Driver
|
||||||
|
username: sa
|
||||||
|
password:
|
||||||
|
jpa:
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: none
|
||||||
|
flyway:
|
||||||
|
enabled: false
|
||||||
Reference in New Issue
Block a user