Compare commits
14 Commits
main
...
ea866377bc
| Author | SHA1 | Date | |
|---|---|---|---|
| ea866377bc | |||
| a7b984ca84 | |||
| 1ba85e129e | |||
| c75bf2ad71 | |||
| d91f39d087 | |||
| f39bf049a0 | |||
| 5b2648d526 | |||
| a8ecf65180 | |||
| 4d96524adb | |||
| 1d5006fd7e | |||
| 406a041ce9 | |||
| d7233d817c | |||
| 92aaf63c12 | |||
| ef09a3c84d |
@@ -8,18 +8,13 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class CorsConfig implements WebMvcConfigurer {
|
public class CorsConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Value("${vaessl.frontend-local-url}")
|
@Value("${vaessl.allowed-origins}")
|
||||||
private String frontendLocalUrl;
|
private String[] allowedOrigins;
|
||||||
|
|
||||||
@Value("${vaessl.frontend-public-url}")
|
|
||||||
private String frontendPublicUrl;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**").allowedOrigins(allowedOrigins)
|
||||||
.allowedOrigins(frontendLocalUrl, frontendPublicUrl)
|
|
||||||
.allowedMethods("GET", "POST", "DELETE", "OPTIONS")
|
.allowedMethods("GET", "POST", "DELETE", "OPTIONS")
|
||||||
.allowedHeaders("Content-Type", "Accept")
|
.allowedHeaders("Content-Type", "Accept").allowCredentials(true);
|
||||||
.allowCredentials(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.vaessl.app.dto;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public record AuthResponse(String serviceType, Instant expiresAt) {}
|
public record AuthResponse(String serviceType, Instant expiresAt) {
|
||||||
|
}
|
||||||
@@ -14,11 +14,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.vaessl.app.dto.AuthResponse;
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.dto.ConnectionStatusResponse;
|
|
||||||
import com.vaessl.app.dto.LoginResult;
|
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -33,8 +28,7 @@ public class ConnectionController {
|
|||||||
private final ConnectionService connectionService;
|
private final ConnectionService connectionService;
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public ResponseEntity<AuthResponse> login(
|
public ResponseEntity<AuthResponse> login(@Valid @RequestBody ConnectionRequest request,
|
||||||
@Valid @RequestBody ConnectionRequest request,
|
|
||||||
HttpServletRequest httpReq) {
|
HttpServletRequest httpReq) {
|
||||||
|
|
||||||
LoginResult result = connectionService.login(request);
|
LoginResult result = connectionService.login(request);
|
||||||
@@ -58,21 +52,21 @@ public class ConnectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<ConnectionStatusResponse> statuses = new ArrayList<>();
|
List<ConnectionStatusResponse> statuses = new ArrayList<>();
|
||||||
Collections.list(session.getAttributeNames()).stream()
|
Collections.list(session.getAttributeNames()).stream().filter(k -> k.endsWith(SUFFIX))
|
||||||
.filter(k -> k.endsWith(SUFFIX))
|
|
||||||
.forEach(k -> {
|
.forEach(k -> {
|
||||||
String serviceType = k.replace(SUFFIX, "");
|
String serviceType = k.replace(SUFFIX, "");
|
||||||
Long id = (Long) session.getAttribute(k);
|
Long id = (Long) session.getAttribute(k);
|
||||||
ConnectionStatusResponse status = connectionService.getConnectionStatus(serviceType, id);
|
ConnectionStatusResponse status =
|
||||||
if (status != null) statuses.add(status);
|
connectionService.getConnectionStatus(serviceType, id);
|
||||||
|
if (status != null)
|
||||||
|
statuses.add(status);
|
||||||
});
|
});
|
||||||
|
|
||||||
return ResponseEntity.ok(statuses);
|
return ResponseEntity.ok(statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/connections/{serviceType}")
|
@DeleteMapping("/connections/{serviceType}")
|
||||||
public ResponseEntity<Void> logout(
|
public ResponseEntity<Void> logout(@PathVariable("serviceType") String serviceType,
|
||||||
@PathVariable("serviceType") String serviceType,
|
|
||||||
HttpServletRequest httpReq) {
|
HttpServletRequest httpReq) {
|
||||||
|
|
||||||
HttpSession session = httpReq.getSession(false);
|
HttpSession session = httpReq.getSession(false);
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "connections", uniqueConstraints = { @UniqueConstraint(columnNames = { "appUrl", "username" }) })
|
@Table(name = "connections",
|
||||||
|
uniqueConstraints = {@UniqueConstraint(columnNames = {"appUrl", "username"})})
|
||||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||||
@DiscriminatorColumn(name = "service_type")
|
@DiscriminatorColumn(name = "service_type")
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@@ -2,15 +2,10 @@ package com.vaessl.app.connection;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
public interface ConnectionProvider extends ServiceProvider {
|
||||||
import com.vaessl.app.dto.ConnectionResponse;
|
|
||||||
|
|
||||||
public interface ConnectionProvider {
|
|
||||||
|
|
||||||
void checkCredentials(ConnectionRequest request);
|
void checkCredentials(ConnectionRequest request);
|
||||||
|
|
||||||
String getServiceType();
|
|
||||||
|
|
||||||
ConnectionResponse authenticate(ConnectionRequest request);
|
ConnectionResponse authenticate(ConnectionRequest request);
|
||||||
|
|
||||||
ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request);
|
ConnectionEntity findUniqueConnectionEntry(ConnectionRequest request);
|
||||||
|
|||||||
+5
-8
@@ -1,15 +1,12 @@
|
|||||||
package com.vaessl.app.dto;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
public record ConnectionRequest(
|
public record ConnectionRequest(@NotBlank(message = "App URL is mandatory") String appUrl,
|
||||||
@NotBlank(message = "App URL is mandatory") String appUrl,
|
|
||||||
@NotBlank(message = "Service type is mandatory") String serviceType,
|
@NotBlank(message = "Service type is mandatory") String serviceType,
|
||||||
String username,
|
String username, String password, String apiKey,
|
||||||
String password,
|
|
||||||
String apiKey,
|
|
||||||
@JsonProperty(defaultValue = "false") Boolean stayLoggedIn) {
|
@JsonProperty(defaultValue = "false") Boolean stayLoggedIn) {
|
||||||
|
|
||||||
public ConnectionRequest {
|
public ConnectionRequest {
|
||||||
@@ -18,8 +15,8 @@ public record ConnectionRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionRequest(String appUrl, String serviceType, String username, String password,
|
public ConnectionRequest(String appUrl, String serviceType, String username,
|
||||||
Boolean stayLoggedIn) {
|
String password, Boolean stayLoggedIn) {
|
||||||
this(appUrl, serviceType, username, password, null, stayLoggedIn);
|
this(appUrl, serviceType, username, password, null, stayLoggedIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-4
@@ -1,12 +1,13 @@
|
|||||||
package com.vaessl.app.dto;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public record ConnectionResponse(String token, Instant expiresAt, Map<String, Object> extraResponseData) {
|
public record ConnectionResponse(String token, Instant expiresAt,
|
||||||
|
Map<String, Object> extraResponseData) {
|
||||||
|
|
||||||
public String getExtraVar(String key) {
|
public String getExtraVar(String key) {
|
||||||
if(extraResponseData == null) {
|
if (extraResponseData == null) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
Object value = extraResponseData.get(key);
|
Object value = extraResponseData.get(key);
|
||||||
@@ -7,10 +7,6 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.dto.ConnectionResponse;
|
|
||||||
import com.vaessl.app.dto.ConnectionStatusResponse;
|
|
||||||
import com.vaessl.app.dto.LoginResult;
|
|
||||||
import com.vaessl.app.exception.WrongServiceTypeException;
|
import com.vaessl.app.exception.WrongServiceTypeException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -54,13 +50,14 @@ public class ConnectionService {
|
|||||||
|
|
||||||
public ConnectionStatusResponse getConnectionStatus(String serviceType, Long connectionId) {
|
public ConnectionStatusResponse getConnectionStatus(String serviceType, Long connectionId) {
|
||||||
ConnectionEntity entity = cRepository.findById(connectionId).orElse(null);
|
ConnectionEntity entity = cRepository.findById(connectionId).orElse(null);
|
||||||
if (entity == null) return null;
|
if (entity == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
ConnectionProvider provider = providerRegistry.get(serviceType);
|
ConnectionProvider provider = providerRegistry.get(serviceType);
|
||||||
Instant expiresAt = (provider != null) ? provider.getTokenExpiry(entity) : null;
|
Instant expiresAt = (provider != null) ? provider.getTokenExpiry(entity) : null;
|
||||||
boolean connected = expiresAt == null || expiresAt.isAfter(Instant.now());
|
boolean connected = expiresAt == null || expiresAt.isAfter(Instant.now());
|
||||||
|
|
||||||
return new ConnectionStatusResponse(serviceType, entity.getAppUrl(),
|
return new ConnectionStatusResponse(serviceType, entity.getAppUrl(), entity.getUsername(),
|
||||||
entity.getUsername(), expiresAt, connected);
|
expiresAt, connected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public record ConnectionStatusResponse(String serviceType, String appUrl, String username,
|
||||||
|
Instant expiresAt, boolean connected) {
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
public enum Endpoint {
|
public enum Endpoint {
|
||||||
HOMEBOX_LOGIN("/api/v1/users/login"),
|
HOMEBOX_LOGIN("/api/v1/users/login"), LOGIN("/login"), CONNECTION_STATUS(
|
||||||
LOGIN("/login"),
|
"/connections/status"), HOMEBOX_QUERY_ALL_ITEMS("/api/v1/items"), SEARCH("/search");
|
||||||
CONNECTION_STATUS("/connections/status");
|
|
||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
|
|||||||
+17
-16
@@ -9,20 +9,20 @@ import java.util.Map;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.dto.ConnectionResponse;
|
|
||||||
import com.vaessl.app.exception.EmptyCredentialsException;
|
import com.vaessl.app.exception.EmptyCredentialsException;
|
||||||
|
import com.vaessl.app.exception.RemoteApiException;
|
||||||
|
|
||||||
import static com.vaessl.app.connection.Endpoint.*;
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class HomeBoxConnectionProvider implements ConnectionProvider {
|
public class HomeboxConnectionProvider implements ConnectionProvider {
|
||||||
|
|
||||||
private final RestClient.Builder restClientBuilder;
|
private final RestClient.Builder restClientBuilder;
|
||||||
|
|
||||||
private final ConnectionRepository cRepository;
|
private final ConnectionRepository cRepository;
|
||||||
|
|
||||||
public HomeBoxConnectionProvider(RestClient.Builder restClientBuilder, ConnectionRepository cRepository) {
|
public HomeboxConnectionProvider(RestClient.Builder restClientBuilder,
|
||||||
|
ConnectionRepository cRepository) {
|
||||||
this.restClientBuilder = restClientBuilder;
|
this.restClientBuilder = restClientBuilder;
|
||||||
this.cRepository = cRepository;
|
this.cRepository = cRepository;
|
||||||
}
|
}
|
||||||
@@ -50,27 +50,26 @@ public class HomeBoxConnectionProvider implements ConnectionProvider {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConnectionResponse authenticate(ConnectionRequest request) {
|
public ConnectionResponse authenticate(ConnectionRequest request) {
|
||||||
Map<String, Object> homeboxPayload = Map.of("username", request.username(),
|
Map<String, Object> homeboxPayload = Map.of("username", request.username(), "password",
|
||||||
"password", request.password(), "stayLoggedIn",
|
request.password(), "stayLoggedIn", request.stayLoggedIn());
|
||||||
request.stayLoggedIn());
|
|
||||||
|
|
||||||
HomeboxLoginResponse hbResponse = restClientBuilder.baseUrl(request.appUrl())
|
HomeboxLoginResponse hbResponse = restClientBuilder.baseUrl(request.appUrl()).build().post()
|
||||||
.build()
|
.uri(HOMEBOX_LOGIN.getValue()).body(homeboxPayload).retrieve()
|
||||||
.post()
|
|
||||||
.uri(HOMEBOX_LOGIN.getValue())
|
|
||||||
.body(homeboxPayload)
|
|
||||||
.retrieve()
|
|
||||||
.body(HomeboxLoginResponse.class);
|
.body(HomeboxLoginResponse.class);
|
||||||
|
|
||||||
if (hbResponse == null) {
|
if (hbResponse == null) {
|
||||||
throw new IllegalStateException("Remote API returned an empty body for " + request.appUrl());
|
throw new RemoteApiException(request.appUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> attachmentToken = new HashMap<>();
|
Map<String, Object> attachmentToken = new HashMap<>();
|
||||||
|
|
||||||
attachmentToken.put("attachmentToken", hbResponse.attachmentToken());
|
attachmentToken.put("attachmentToken", hbResponse.attachmentToken());
|
||||||
|
|
||||||
return new ConnectionResponse(hbResponse.token(), hbResponse.expiresAt(), attachmentToken);
|
String hbRawToken = hbResponse.token();
|
||||||
|
|
||||||
|
String token = hbRawToken.startsWith("Bearer ") ? hbRawToken.substring(7) : hbRawToken;
|
||||||
|
|
||||||
|
return new ConnectionResponse(token, hbResponse.expiresAt(), attachmentToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,7 +79,8 @@ public class HomeBoxConnectionProvider implements ConnectionProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ConnectionEntity connectionToEntity(ConnectionRequest request, ConnectionResponse response) {
|
public ConnectionEntity connectionToEntity(ConnectionRequest request,
|
||||||
|
ConnectionResponse response) {
|
||||||
return HomeboxEntity.from(request, response);
|
return HomeboxEntity.from(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,4 +105,5 @@ public class HomeBoxConnectionProvider implements ConnectionProvider {
|
|||||||
private record HomeboxLoginResponse(String token, String attachmentToken, Instant expiresAt) {
|
private record HomeboxLoginResponse(String token, String attachmentToken, Instant expiresAt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,6 @@ package com.vaessl.app.connection;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.dto.ConnectionResponse;
|
|
||||||
|
|
||||||
import jakarta.persistence.DiscriminatorValue;
|
import jakarta.persistence.DiscriminatorValue;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|||||||
+3
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.vaessl.app.dto;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public record LoginResult(Long connectionId, Instant expiresAt) {}
|
public record LoginResult(Long connectionId, Instant expiresAt) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
public interface ServiceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the service type key used to look up this provider in a registry, e.g.
|
||||||
|
* {@code "HOMEBOX"}.
|
||||||
|
*/
|
||||||
|
String getServiceType();
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ public enum ServiceType {
|
|||||||
|
|
||||||
private final String value;
|
private final String value;
|
||||||
|
|
||||||
private ServiceType(String value){
|
private ServiceType(String value) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.vaessl.app.dto;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
public record ConnectionStatusResponse(
|
|
||||||
String serviceType,
|
|
||||||
String appUrl,
|
|
||||||
String username,
|
|
||||||
Instant expiresAt,
|
|
||||||
boolean connected) {}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.vaessl.app.exception;
|
||||||
|
|
||||||
|
public class ConnectionNotFoundException extends RuntimeException {
|
||||||
|
}
|
||||||
@@ -5,11 +5,18 @@ import org.springframework.http.HttpStatus;
|
|||||||
import com.fasterxml.jackson.annotation.JsonValue;
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
public enum ErrorMessage {
|
public enum ErrorMessage {
|
||||||
BAD_REQUEST_EMPTY_FIELDS(HttpStatus.BAD_REQUEST, "Fields must not be empty."), UNAUTHORIZED_WRONG_LOGIN(
|
BAD_REQUEST_EMPTY_FIELDS(HttpStatus.BAD_REQUEST,
|
||||||
HttpStatus.UNAUTHORIZED, "Invalid username or password."), SERVICE_UNAVAILABLE_UNREACHABLE_URL(
|
"Fields must not be empty."), UNAUTHORIZED_WRONG_LOGIN(HttpStatus.UNAUTHORIZED,
|
||||||
HttpStatus.SERVICE_UNAVAILABLE, "The target URL is unreachable."), SERVER_ERROR_GENERAL(
|
"Invalid username or password."), SERVICE_UNAVAILABLE_UNREACHABLE_URL(
|
||||||
"The external app returned a server error: "), WRONG_SERVICE_TYPE(HttpStatus.NOT_FOUND,
|
HttpStatus.SERVICE_UNAVAILABLE,
|
||||||
"No such service type.");
|
"The target URL is unreachable."), SERVER_ERROR_GENERAL(
|
||||||
|
"The external app returned a server error: "), WRONG_SERVICE_TYPE(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"No such service type."), CONNECTION_NOT_FOUND(
|
||||||
|
HttpStatus.NOT_FOUND,
|
||||||
|
"No active connection found for this service."), REMOTE_API_EMPTY_RESPONSE(
|
||||||
|
HttpStatus.BAD_GATEWAY,
|
||||||
|
"Remote API returned empty response for ");
|
||||||
|
|
||||||
private final HttpStatus status;
|
private final HttpStatus status;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
public ProblemDetail handleEmptyCredentialInput(MethodArgumentNotValidException e) {
|
public ProblemDetail handleEmptyCredentialInput(MethodArgumentNotValidException e) {
|
||||||
|
|
||||||
String defaultMessages = e.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage)
|
String defaultMessages = e.getBindingResult().getFieldErrors().stream()
|
||||||
.collect(Collectors.joining(", "));
|
.map(FieldError::getDefaultMessage).collect(Collectors.joining(", "));
|
||||||
|
|
||||||
return ProblemDetail.forStatusAndDetail(BAD_REQUEST_EMPTY_FIELDS.getStatus(),
|
return ProblemDetail.forStatusAndDetail(BAD_REQUEST_EMPTY_FIELDS.getStatus(),
|
||||||
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " [" + defaultMessages + "]");
|
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " [" + defaultMessages + "]");
|
||||||
@@ -43,14 +43,14 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(HttpServerErrorException.class)
|
@ExceptionHandler(HttpServerErrorException.class)
|
||||||
public ProblemDetail handleTimeoutOrNotFound(HttpServerErrorException e) {
|
public ProblemDetail handleTimeoutOrNotFound(HttpServerErrorException e) {
|
||||||
|
|
||||||
return ProblemDetail
|
return ProblemDetail.forStatusAndDetail(e.getStatusCode(),
|
||||||
.forStatusAndDetail(e.getStatusCode(),
|
SERVER_ERROR_GENERAL.getMessage() + e.getStatusText());
|
||||||
SERVER_ERROR_GENERAL.getMessage() + e.getStatusText());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(WrongServiceTypeException.class)
|
@ExceptionHandler(WrongServiceTypeException.class)
|
||||||
public ProblemDetail handleWrongServiceType(WrongServiceTypeException e) {
|
public ProblemDetail handleWrongServiceType(WrongServiceTypeException e) {
|
||||||
return ProblemDetail.forStatusAndDetail(WRONG_SERVICE_TYPE.getStatus(), WRONG_SERVICE_TYPE.getMessage());
|
return ProblemDetail.forStatusAndDetail(WRONG_SERVICE_TYPE.getStatus(),
|
||||||
|
WRONG_SERVICE_TYPE.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(EmptyCredentialsException.class)
|
@ExceptionHandler(EmptyCredentialsException.class)
|
||||||
@@ -58,4 +58,15 @@ public class GlobalExceptionHandler {
|
|||||||
return ProblemDetail.forStatusAndDetail(BAD_REQUEST_EMPTY_FIELDS.getStatus(),
|
return ProblemDetail.forStatusAndDetail(BAD_REQUEST_EMPTY_FIELDS.getStatus(),
|
||||||
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " " + e.getMissingFields());
|
BAD_REQUEST_EMPTY_FIELDS.getMessage() + " " + e.getMissingFields());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(ConnectionNotFoundException.class)
|
||||||
|
public ProblemDetail handleConnectionNotFound(ConnectionNotFoundException e) {
|
||||||
|
return ProblemDetail.forStatusAndDetail(CONNECTION_NOT_FOUND.getStatus(), CONNECTION_NOT_FOUND.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(RemoteApiException.class)
|
||||||
|
public ProblemDetail handleRemoteApiException(RemoteApiException e) {
|
||||||
|
return ProblemDetail.forStatusAndDetail(REMOTE_API_EMPTY_RESPONSE.getStatus(),
|
||||||
|
REMOTE_API_EMPTY_RESPONSE.getMessage() + e.getAppUrl());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.vaessl.app.exception;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RemoteApiException extends RuntimeException {
|
||||||
|
|
||||||
|
private final String appUrl;
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.client.RestClient;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ConnectionEntity;
|
||||||
|
import com.vaessl.app.connection.ConnectionRepository;
|
||||||
|
import com.vaessl.app.connection.HomeboxEntity;
|
||||||
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
|
import com.vaessl.app.exception.RemoteApiException;
|
||||||
|
|
||||||
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class HomeboxSearchProvider implements SearchProvider {
|
||||||
|
|
||||||
|
private final RestClient.Builder restClientBuilder;
|
||||||
|
|
||||||
|
private final ConnectionRepository cRepository;
|
||||||
|
|
||||||
|
public HomeboxSearchProvider(RestClient.Builder restClientBuilder,
|
||||||
|
ConnectionRepository cRepository) {
|
||||||
|
this.restClientBuilder = restClientBuilder;
|
||||||
|
this.cRepository = cRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServiceType() {
|
||||||
|
return "HOMEBOX";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<SearchResponse> getSearchResults(SearchRequest request, Pageable pageable) {
|
||||||
|
|
||||||
|
ConnectionEntity entity =
|
||||||
|
cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
|
||||||
|
|
||||||
|
if (!(entity instanceof HomeboxEntity hbEntity)) {
|
||||||
|
throw new ConnectionNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeboxSearchResponse hbResponse = restClientBuilder.baseUrl(request.appUrl()).build().get()
|
||||||
|
.uri(u -> u.path(HOMEBOX_QUERY_ALL_ITEMS.getValue())
|
||||||
|
.queryParam("q", request.query())
|
||||||
|
.queryParam("page", pageable.getPageNumber() + 1)
|
||||||
|
.queryParam("pageSize", pageable.getPageSize()).build())
|
||||||
|
.headers(h -> h.setBearerAuth(
|
||||||
|
hbEntity.getToken())).retrieve().body(HomeboxSearchResponse.class);
|
||||||
|
|
||||||
|
if (hbResponse == null) {
|
||||||
|
throw new RemoteApiException(request.appUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<SearchResponse> items = hbResponse.items().stream().map(i -> {
|
||||||
|
String title = i.name();
|
||||||
|
String description = i.description();
|
||||||
|
Map<String, Object> extraSearchResponseData = Map.of("location", i.location());
|
||||||
|
return new SearchResponse(title, description, extraSearchResponseData);
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return new PageImpl<>(items, pageable, hbResponse.total());
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxSearchResponse(int page, int pageSize, int total,
|
||||||
|
List<HomeboxItem> items) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxItem(String name, String description, HomeboxLocation location) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record HomeboxLocation(String name, String description) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
public record PagedSearchResponse<T>(List<T> content, int page, int pageSize, long totalElements,
|
||||||
|
boolean first, boolean last, String sort) {
|
||||||
|
|
||||||
|
public static <T> PagedSearchResponse<T> from(Page<T> pageResult) {
|
||||||
|
return new PagedSearchResponse<>(pageResult.getContent(), pageResult.getNumber(),
|
||||||
|
pageResult.getSize(), pageResult.getTotalElements(), pageResult.isFirst(),
|
||||||
|
pageResult.isLast(), pageResult.getSort().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SearchController {
|
||||||
|
|
||||||
|
private final SearchService searchService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a paged search against the requested service. Returns {@code 401 Unauthorized} if
|
||||||
|
* there is no active session.
|
||||||
|
*/
|
||||||
|
@PostMapping("/search")
|
||||||
|
public ResponseEntity<PagedSearchResponse<SearchResponse>> search(
|
||||||
|
@Valid @RequestBody SearchRequest request,
|
||||||
|
@PageableDefault(size = 20) Pageable pageable, HttpServletRequest httpReq) {
|
||||||
|
HttpSession session = httpReq.getSession(false);
|
||||||
|
if (session == null
|
||||||
|
|| session.getAttribute(request.serviceType() + "_CONNECTION_ID") == null) {
|
||||||
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
Page<SearchResponse> result = searchService.search(request, pageable);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(PagedSearchResponse.from(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ServiceProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implemented by any service that supports querying its remote API for items.
|
||||||
|
*/
|
||||||
|
public interface SearchProvider extends ServiceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a search query against the remote service and returns matching results.
|
||||||
|
*
|
||||||
|
* @param request the search request containing the query string, app URL, and user credentials
|
||||||
|
* @return a list of {@link SearchResponse} items matching the query
|
||||||
|
*/
|
||||||
|
Page<SearchResponse> getSearchResults(SearchRequest request, Pageable pageable);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
||||||
|
@NotBlank String serviceType) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public record SearchResponse(String title, String description, Map<String, Object> extraData) {
|
||||||
|
|
||||||
|
public String getExtra(String key) {
|
||||||
|
if (extraData == null) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
Object value = extraData.get(key);
|
||||||
|
|
||||||
|
return value != null ? String.valueOf(value) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.vaessl.app.exception.WrongServiceTypeException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SearchService {
|
||||||
|
|
||||||
|
private final Map<String, SearchProvider> providerRegistry;
|
||||||
|
|
||||||
|
public SearchService(List<SearchProvider> providers) {
|
||||||
|
this.providerRegistry = Map.copyOf(providers.stream()
|
||||||
|
.collect(Collectors.toMap(SearchProvider::getServiceType, p -> p)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches the paged search request to the provider registered for
|
||||||
|
* {@link SearchRequest#serviceType()}.
|
||||||
|
*
|
||||||
|
* @param request the search request
|
||||||
|
* @param pageable the Pageable interface
|
||||||
|
* @return results returned by the matching provider
|
||||||
|
* @throws WrongServiceTypeException if no provider is registered for the given service type
|
||||||
|
*/
|
||||||
|
public Page<SearchResponse> search(SearchRequest request, Pageable pageable) {
|
||||||
|
|
||||||
|
SearchProvider provider = providerRegistry.get(request.serviceType());
|
||||||
|
|
||||||
|
if (provider == null) {
|
||||||
|
throw new WrongServiceTypeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider.getSearchResults(request, pageable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,8 @@
|
|||||||
"description": "A description for 'spring.session.store-type'"
|
"description": "A description for 'spring.session.store-type'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "vaessl.frontend-local-url",
|
"name": "vaessl.allowed-origins",
|
||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
"description": "A description for 'vaessl.frontend-local-url'"
|
"description": "Comma-separated list of allowed CORS origins"
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vaessl.frontend-public-url",
|
|
||||||
"type": "java.lang.String",
|
|
||||||
"description": "A description for 'vaessl.frontend-public-url'"
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
@@ -13,7 +13,7 @@ spring:
|
|||||||
driver-class-name: ${PG_DRIVER_CLASS_NAME}
|
driver-class-name: ${PG_DRIVER_CLASS_NAME}
|
||||||
jpa:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: create-drop
|
ddl-auto: update
|
||||||
show-sql: true
|
show-sql: true
|
||||||
ai:
|
ai:
|
||||||
openai:
|
openai:
|
||||||
@@ -30,5 +30,4 @@ server:
|
|||||||
servlet:
|
servlet:
|
||||||
context-path: /api
|
context-path: /api
|
||||||
vaessl:
|
vaessl:
|
||||||
frontend-local-url: ${FRONTEND_LOCAL_URL}
|
allowed-origins: ${ALLOWED_ORIGINS}
|
||||||
frontend-public-url: ${FRONTEND_PUBLIC_URL}
|
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ class ApplicationTests {
|
|||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void contextLoads() {
|
void contextLoads() {}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void connectionToTestDbWorks() throws SQLException {
|
void connectionToTestDbWorks() throws SQLException {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.vaessl.app;
|
||||||
|
|
||||||
|
public final class Mockdata {
|
||||||
|
|
||||||
|
private Mockdata() {}
|
||||||
|
|
||||||
|
public static final String MOCK_URL = "http://localhost:1234";
|
||||||
|
public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE";
|
||||||
|
public static final String MOCK_USER = "user";
|
||||||
|
public static final String MOCK_PASS = "pw";
|
||||||
|
public static final String MOCK_TITLE = "title";
|
||||||
|
public static final String MOCK_DESCRIPTION = "desc";
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
|
|
||||||
import static com.vaessl.app.connection.Endpoint.*;
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
import static com.vaessl.app.connection.ServiceType.*;
|
import static com.vaessl.app.connection.ServiceType.*;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
@@ -24,134 +26,129 @@ import org.springframework.test.web.servlet.MvcResult;
|
|||||||
@WireMockTest
|
@WireMockTest
|
||||||
class ConnectionControllerTest {
|
class ConnectionControllerTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
MockMvc mockMvc;
|
MockMvc mockMvc;
|
||||||
|
|
||||||
private static final String TEST_USER = "admin";
|
private static final String LOGIN_PATH = LOGIN.getValue();
|
||||||
private static final String TEST_PASS = "pw";
|
private static final String STATUS_PATH = CONNECTION_STATUS.getValue();
|
||||||
private static final String LOGIN_PATH = LOGIN.getValue();
|
private static final String LOGOUT_PATH = "/connections/HOMEBOX";
|
||||||
private static final String STATUS_PATH = CONNECTION_STATUS.getValue();
|
|
||||||
private static final String LOGOUT_PATH = "/connections/HOMEBOX";
|
|
||||||
|
|
||||||
private static final String VALID_HOMEBOX_RESPONSE = """
|
private static final String VALID_HOMEBOX_RESPONSE = """
|
||||||
{
|
{
|
||||||
"token": "fake-jwt-token",
|
"token": "fake-jwt-token",
|
||||||
"attachmentToken": "fake-attach",
|
"attachmentToken": "fake-attach",
|
||||||
"expiresAt": "2099-01-01T00:00:00Z"
|
"expiresAt": "2099-01-01T00:00:00Z"
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static final String EXPIRED_HOMEBOX_RESPONSE = """
|
private static final String EXPIRED_HOMEBOX_RESPONSE = """
|
||||||
{
|
{
|
||||||
"token": "expired-token",
|
"token": "expired-token",
|
||||||
"attachmentToken": "fake-attach",
|
"attachmentToken": "fake-attach",
|
||||||
"expiresAt": "2000-01-01T00:00:00Z"
|
"expiresAt": "2000-01-01T00:00:00Z"
|
||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyListWhenNoActiveSession() throws Exception {
|
void shouldReturnEmptyListWhenNoActiveSession() throws Exception {
|
||||||
mockMvc.perform(get(STATUS_PATH))
|
mockMvc.perform(get(STATUS_PATH)).andExpect(status().isOk())
|
||||||
.andExpect(status().isOk())
|
.andExpect(content().json("[]"));
|
||||||
.andExpect(content().json("[]"));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnConnectionStatusWithConnectedTrueAfterLogin(WireMockRuntimeInfo wm) throws Exception {
|
void shouldReturnConnectionStatusWithConnectedTrueAfterLogin(WireMockRuntimeInfo wm)
|
||||||
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue()).willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
throws Exception {
|
||||||
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
||||||
|
|
||||||
MvcResult loginResult = mockMvc.perform(post(LOGIN_PATH)
|
MvcResult loginResult = mockMvc
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk()).andReturn();
|
||||||
.andReturn();
|
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie))
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
||||||
.andExpect(status().isOk())
|
.andExpect(jsonPath("$.length()").value(1))
|
||||||
.andExpect(jsonPath("$.length()").value(1))
|
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.getValue()))
|
||||||
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.getValue()))
|
.andExpect(jsonPath("$[0].username").value(MOCK_USER))
|
||||||
.andExpect(jsonPath("$[0].username").value(TEST_USER))
|
.andExpect(jsonPath("$[0].appUrl").value(wm.getHttpBaseUrl()))
|
||||||
.andExpect(jsonPath("$[0].appUrl").value(wm.getHttpBaseUrl()))
|
.andExpect(jsonPath("$[0].connected").value(true));
|
||||||
.andExpect(jsonPath("$[0].connected").value(true));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnConnectedFalseWhenStoredTokenIsExpired(WireMockRuntimeInfo wm) throws Exception {
|
void shouldReturnConnectedFalseWhenStoredTokenIsExpired(WireMockRuntimeInfo wm)
|
||||||
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue()).willReturn(WireMock.okJson(EXPIRED_HOMEBOX_RESPONSE)));
|
throws Exception {
|
||||||
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(EXPIRED_HOMEBOX_RESPONSE)));
|
||||||
|
|
||||||
MvcResult loginResult = mockMvc.perform(post(LOGIN_PATH)
|
MvcResult loginResult = mockMvc
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk()).andReturn();
|
||||||
.andReturn();
|
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie))
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
||||||
.andExpect(status().isOk())
|
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.getValue()))
|
||||||
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.getValue()))
|
.andExpect(jsonPath("$[0].connected").value(false))
|
||||||
.andExpect(jsonPath("$[0].connected").value(false))
|
.andExpect(jsonPath("$[0].expiresAt")
|
||||||
.andExpect(jsonPath("$[0].expiresAt").value("2000-01-01T00:00:00Z"));
|
.value("2000-01-01T00:00:00Z"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturn204NoContentOnLogout(WireMockRuntimeInfo wm) throws Exception {
|
void shouldReturn204NoContentOnLogout(WireMockRuntimeInfo wm) throws Exception {
|
||||||
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue()).willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
||||||
|
|
||||||
MvcResult loginResult = mockMvc.perform(post(LOGIN_PATH)
|
MvcResult loginResult = mockMvc
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk()).andReturn();
|
||||||
.andReturn();
|
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
mockMvc.perform(delete(LOGOUT_PATH).cookie(sessionCookie))
|
mockMvc.perform(delete(LOGOUT_PATH).cookie(sessionCookie))
|
||||||
.andExpect(status().isNoContent());
|
.andExpect(status().isNoContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyStatusListAfterLogout(WireMockRuntimeInfo wm) throws Exception {
|
void shouldReturnEmptyStatusListAfterLogout(WireMockRuntimeInfo wm) throws Exception {
|
||||||
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue()).willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_RESPONSE)));
|
||||||
|
|
||||||
MvcResult loginResult = mockMvc.perform(post(LOGIN_PATH)
|
MvcResult loginResult = mockMvc
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk()).andReturn();
|
||||||
.andReturn();
|
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
// Verify connected before logout
|
// Verify connected before logout
|
||||||
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie))
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
||||||
.andExpect(status().isOk())
|
.andExpect(jsonPath("$.length()").value(1));
|
||||||
.andExpect(jsonPath("$.length()").value(1));
|
|
||||||
|
|
||||||
mockMvc.perform(delete(LOGOUT_PATH).cookie(sessionCookie))
|
mockMvc.perform(delete(LOGOUT_PATH).cookie(sessionCookie))
|
||||||
.andExpect(status().isNoContent());
|
.andExpect(status().isNoContent());
|
||||||
|
|
||||||
// A new request (no session cookie, as in a fresh browser) returns no connections
|
// A new request (no session cookie, as in a fresh browser) returns no connections
|
||||||
mockMvc.perform(get(STATUS_PATH))
|
mockMvc.perform(get(STATUS_PATH)).andExpect(status().isOk())
|
||||||
.andExpect(status().isOk())
|
.andExpect(content().json("[]"));
|
||||||
.andExpect(content().json("[]"));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturn204WhenLogoutCalledWithNoActiveSession() throws Exception {
|
void shouldReturn204WhenLogoutCalledWithNoActiveSession() throws Exception {
|
||||||
mockMvc.perform(delete(LOGOUT_PATH))
|
mockMvc.perform(delete(LOGOUT_PATH)).andExpect(status().isNoContent());
|
||||||
.andExpect(status().isNoContent());
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private String connectionRequestBody(WireMockRuntimeInfo wm) {
|
private String connectionRequestBody(WireMockRuntimeInfo wm) {
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"appUrl": "%s",
|
"appUrl": "%s",
|
||||||
"serviceType": "HOMEBOX",
|
"serviceType": "HOMEBOX",
|
||||||
"username": "%s",
|
"username": "%s",
|
||||||
"password": "%s"
|
"password": "%s"
|
||||||
}
|
}
|
||||||
""".formatted(wm.getHttpBaseUrl(), TEST_USER, TEST_PASS);
|
""".formatted(wm.getHttpBaseUrl(), MOCK_USER, MOCK_PASS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doThrow;
|
import static org.mockito.Mockito.doThrow;
|
||||||
@@ -15,11 +16,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.exception.EmptyCredentialsException;
|
import com.vaessl.app.exception.EmptyCredentialsException;
|
||||||
|
|
||||||
import static com.vaessl.app.connection.Mockdata.*;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ConnectionServiceTest {
|
class ConnectionServiceTest {
|
||||||
|
|
||||||
@@ -38,13 +36,14 @@ class ConnectionServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void login_ShouldAbort_WhenCheckCredentialsThrowsException() {
|
void login_ShouldAbort_WhenCheckCredentialsThrowsException() {
|
||||||
ConnectionRequest request = new ConnectionRequest(MOCK_URL, MOCK_SERVICE_TYPE, null, null, false);
|
ConnectionRequest request =
|
||||||
|
new ConnectionRequest(MOCK_URL, MOCK_SERVICE_TYPE, null, null, false);
|
||||||
|
|
||||||
doThrow(new EmptyCredentialsException(List.of("username")))
|
doThrow(new EmptyCredentialsException(List.of("username"))).when(mockProvider)
|
||||||
.when(mockProvider).checkCredentials(request);
|
.checkCredentials(request);
|
||||||
|
|
||||||
assertThrows(EmptyCredentialsException.class, () -> connectionService.login(request));
|
assertThrows(EmptyCredentialsException.class, () -> connectionService.login(request));
|
||||||
|
|
||||||
verify(mockProvider, never()).authenticate(any());
|
verify(mockProvider, never()).authenticate(any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-5
@@ -4,15 +4,13 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
import com.vaessl.app.exception.EmptyCredentialsException;
|
import com.vaessl.app.exception.EmptyCredentialsException;
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static com.vaessl.app.connection.Mockdata.*;
|
|
||||||
|
|
||||||
class HomeBoxConnectionProviderTest {
|
class HomeboxConnectionProviderTest {
|
||||||
|
|
||||||
private final HomeBoxConnectionProvider provider = new HomeBoxConnectionProvider(null, null);
|
private final HomeboxConnectionProvider provider = new HomeboxConnectionProvider(null, null);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void checkCredentials_ShouldThrowException_WhenFieldsAreMissing() {
|
void checkCredentials_ShouldThrowException_WhenFieldsAreMissing() {
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
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.resttestclient.TestRestTemplate;
|
import org.springframework.boot.resttestclient.TestRestTemplate;
|
||||||
@@ -14,7 +15,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
|||||||
|
|
||||||
import com.jayway.jsonpath.DocumentContext;
|
import com.jayway.jsonpath.DocumentContext;
|
||||||
import com.jayway.jsonpath.JsonPath;
|
import com.jayway.jsonpath.JsonPath;
|
||||||
import com.vaessl.app.dto.ConnectionRequest;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -42,9 +42,6 @@ class HomeboxIntegrationTest {
|
|||||||
}
|
}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
private static final String TEST_USER = "admin";
|
|
||||||
private static final String TEST_PASS = "pw";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns Token and status code OK when login is successful.
|
* Returns Token and status code OK when login is successful.
|
||||||
*
|
*
|
||||||
@@ -53,11 +50,10 @@ class HomeboxIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldReturnStatusOkWhenHomeboxCredentialsAreValid(WireMockRuntimeInfo wm) {
|
void shouldReturnStatusOkWhenHomeboxCredentialsAreValid(WireMockRuntimeInfo wm) {
|
||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue())
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(okJson(okJsonHomeboxResponse)));
|
||||||
.willReturn(okJson(okJsonHomeboxResponse)));
|
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm),
|
ResponseEntity<String> response =
|
||||||
String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
||||||
|
|
||||||
DocumentContext documentContext = JsonPath.parse(response.getBody());
|
DocumentContext documentContext = JsonPath.parse(response.getBody());
|
||||||
|
|
||||||
@@ -79,8 +75,8 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(unauthorized()));
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(unauthorized()));
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm),
|
ResponseEntity<String> response =
|
||||||
String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
|
||||||
assertThat(response.getBody()).contains(UNAUTHORIZED_WRONG_LOGIN.getMessage());
|
assertThat(response.getBody()).contains(UNAUTHORIZED_WRONG_LOGIN.getMessage());
|
||||||
@@ -96,8 +92,8 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(serviceUnavailable()));
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(serviceUnavailable()));
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm),
|
ResponseEntity<String> response =
|
||||||
String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
assertThat(response.getBody()).contains(SERVER_ERROR_GENERAL.getMessage());
|
assertThat(response.getBody()).contains(SERVER_ERROR_GENERAL.getMessage());
|
||||||
@@ -113,7 +109,8 @@ class HomeboxIntegrationTest {
|
|||||||
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
|
.willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
|
||||||
|
|
||||||
ConnectionRequest badRequest = connectionRequest(wm);
|
ConnectionRequest badRequest = connectionRequest(wm);
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), badRequest, String.class);
|
ResponseEntity<String> response =
|
||||||
|
restTemplate.postForEntity(LOGIN.getValue(), badRequest, String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
assertThat(response.getBody()).contains(SERVICE_UNAVAILABLE_UNREACHABLE_URL.getMessage());
|
assertThat(response.getBody()).contains(SERVICE_UNAVAILABLE_UNREACHABLE_URL.getMessage());
|
||||||
@@ -127,34 +124,30 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
ConnectionRequest emtpyRequest = new ConnectionRequest("", "", "", "", false);
|
ConnectionRequest emtpyRequest = new ConnectionRequest("", "", "", "", false);
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), emtpyRequest, String.class);
|
ResponseEntity<String> response =
|
||||||
|
restTemplate.postForEntity(LOGIN.getValue(), emtpyRequest, String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
assertThat(response.getBody()).contains(BAD_REQUEST_EMPTY_FIELDS.getMessage());
|
assertThat(response.getBody()).contains(BAD_REQUEST_EMPTY_FIELDS.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the exception when there is an input for serviceType but it's
|
* Test the exception when there is an input for serviceType but it's unsupported.
|
||||||
* unsupported.
|
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnWrongServiceTypeException(WireMockRuntimeInfo wm) {
|
void shouldReturnWrongServiceTypeException(WireMockRuntimeInfo wm) {
|
||||||
ConnectionRequest wrongServiceTypeReq = new ConnectionRequest(
|
ConnectionRequest wrongServiceTypeReq = new ConnectionRequest(wm.getHttpBaseUrl(),
|
||||||
wm.getHttpBaseUrl(),
|
"wrong-service-type", MOCK_USER, MOCK_PASS, false);
|
||||||
"wrong-service-type",
|
|
||||||
TEST_USER, TEST_PASS,
|
|
||||||
false);
|
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), wrongServiceTypeReq,
|
ResponseEntity<String> response =
|
||||||
String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), wrongServiceTypeReq, String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
assertThat(response.getBody()).contains(WRONG_SERVICE_TYPE.getMessage());
|
assertThat(response.getBody()).contains(WRONG_SERVICE_TYPE.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests the succesfull persistance of Homebox credential response to the
|
* Tests the succesfull persistance of Homebox credential response to the database.
|
||||||
* database.
|
|
||||||
*
|
*
|
||||||
* @param wm the WiremockRuntimeInfo object
|
* @param wm the WiremockRuntimeInfo object
|
||||||
*/
|
*/
|
||||||
@@ -166,10 +159,12 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
ConnectionRequest request = connectionRequest(wm);
|
ConnectionRequest request = connectionRequest(wm);
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), request, String.class);
|
ResponseEntity<String> response =
|
||||||
|
restTemplate.postForEntity(LOGIN.getValue(), request, String.class);
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
ConnectionEntity dbEntry = cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
|
ConnectionEntity dbEntry =
|
||||||
|
cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
|
||||||
|
|
||||||
assertThat(dbEntry).isNotNull();
|
assertThat(dbEntry).isNotNull();
|
||||||
assertThat(dbEntry.getAppUrl()).isEqualTo(request.appUrl());
|
assertThat(dbEntry.getAppUrl()).isEqualTo(request.appUrl());
|
||||||
@@ -184,26 +179,24 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyCredentialsExceptionWhenCredsAreMissing(WireMockRuntimeInfo wm) {
|
void shouldReturnEmptyCredentialsExceptionWhenCredsAreMissing(WireMockRuntimeInfo wm) {
|
||||||
ConnectionRequest missingCredentials = new ConnectionRequest(wm.getHttpBaseUrl(), HOMEBOX.getValue(), TEST_USER,
|
ConnectionRequest missingCredentials = new ConnectionRequest(wm.getHttpBaseUrl(),
|
||||||
null,
|
HOMEBOX.getValue(), MOCK_USER, null, false);
|
||||||
false);
|
|
||||||
|
|
||||||
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(), missingCredentials,
|
ResponseEntity<String> response =
|
||||||
String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), missingCredentials, String.class);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
assertThat(response.getBody()).contains(BAD_REQUEST_EMPTY_FIELDS.getMessage());
|
assertThat(response.getBody()).contains(BAD_REQUEST_EMPTY_FIELDS.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a valid connection request with a mock Api through
|
* Creates a valid connection request with a mock Api through WireMockRuntimeInfo.
|
||||||
* WireMockRuntimeInfo.
|
|
||||||
*
|
*
|
||||||
* @param wm the WiremockRuntimeInfo object
|
* @param wm the WiremockRuntimeInfo object
|
||||||
* @return a mock api connection request.
|
* @return a mock api connection request.
|
||||||
*/
|
*/
|
||||||
private ConnectionRequest connectionRequest(WireMockRuntimeInfo wm) {
|
private ConnectionRequest connectionRequest(WireMockRuntimeInfo wm) {
|
||||||
return new ConnectionRequest(wm.getHttpBaseUrl(), HOMEBOX.getValue(), TEST_USER, TEST_PASS,
|
return new ConnectionRequest(wm.getHttpBaseUrl(), HOMEBOX.getValue(), MOCK_USER, MOCK_PASS,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.vaessl.app.connection;
|
|
||||||
|
|
||||||
public final class Mockdata {
|
|
||||||
|
|
||||||
private Mockdata() {}
|
|
||||||
|
|
||||||
public static final String MOCK_URL = "http://localhost:1234";
|
|
||||||
public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE";
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import com.vaessl.app.connection.ConnectionRepository;
|
||||||
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class HomeboxSearchProviderTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ConnectionRepository mockRepo;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private HomeboxSearchProvider provider;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnConnectionNotFoundException() {
|
||||||
|
|
||||||
|
when(mockRepo.findByAppUrlAndUsername(MOCK_URL, MOCK_USER)).thenReturn(null);
|
||||||
|
|
||||||
|
SearchRequest request = new SearchRequest(MOCK_URL, MOCK_USER, "test query", "HOMEBOX");
|
||||||
|
Pageable pageable = PageRequest.of(0, 10);
|
||||||
|
assertThrows(ConnectionNotFoundException.class,
|
||||||
|
() -> provider.getSearchResults(request, pageable));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.MOCK_PASS;
|
||||||
|
import static com.vaessl.app.Mockdata.MOCK_USER;
|
||||||
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.MvcResult;
|
||||||
|
import com.github.tomakehurst.wiremock.client.WireMock;
|
||||||
|
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
|
||||||
|
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureMockMvc
|
||||||
|
@WireMockTest
|
||||||
|
class SearchControllerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
private static final String QUERY_ALL_ITEMS = HOMEBOX_QUERY_ALL_ITEMS.getValue();
|
||||||
|
|
||||||
|
private static final String LOGIN_PATH = LOGIN.getValue();
|
||||||
|
|
||||||
|
private static final String SEARCH_REQUEST = SEARCH.getValue();
|
||||||
|
|
||||||
|
private static final String VALID_HOMEBOX_LOGIN_RESPONSE = """
|
||||||
|
{
|
||||||
|
"token": "fake-bearer-token",
|
||||||
|
"attachmentToken": "fake-attach-token",
|
||||||
|
"expiresAt": "2099-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
private static final String VALID_HOMEBOX_ALL_ITEMS_QUERY_RESPONSE = """
|
||||||
|
{
|
||||||
|
"page": -1,
|
||||||
|
"pageSize": -1,
|
||||||
|
"total": 1,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "c643e7f9-93d0-4b5f-ae4d-e1c2d90389e0",
|
||||||
|
"assetId": "000-001",
|
||||||
|
"name": "MacBook Pro A1398",
|
||||||
|
"description": "Running Linux (Fedora)",
|
||||||
|
"quantity": 1,
|
||||||
|
"insured": false,
|
||||||
|
"archived": false,
|
||||||
|
"createdAt": "2026-05-13T19:52:20.016176Z",
|
||||||
|
"updatedAt": "2026-05-14T12:39:11.836403Z",
|
||||||
|
"purchasePrice": 0,
|
||||||
|
"location": {
|
||||||
|
"id": "b6f60ab8-3a2a-4a8d-a4bf-897d0555f636",
|
||||||
|
"name": "Server Schrank Ikea weiß",
|
||||||
|
"description": "Weißer Ikea Schrank, wo sich der Server befindet.",
|
||||||
|
"createdAt": "2026-05-13T19:55:55.817576Z",
|
||||||
|
"updatedAt": "2026-05-14T12:37:24.396651Z"
|
||||||
|
},
|
||||||
|
"tags": [],
|
||||||
|
"imageId": "cb3e44d5-ccd4-421e-9f5a-f52cd5f40ca6",
|
||||||
|
"thumbnailId": "2bfd53fa-1bf1-483c-8d76-7720464532fa",
|
||||||
|
"soldTime": "0001-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnListOfQueriedHomeboxItems(WireMockRuntimeInfo wm) throws Exception {
|
||||||
|
|
||||||
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_LOGIN_RESPONSE)));
|
||||||
|
|
||||||
|
WireMock.stubFor(WireMock.get(WireMock.urlPathEqualTo(QUERY_ALL_ITEMS))
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_ALL_ITEMS_QUERY_RESPONSE)));
|
||||||
|
|
||||||
|
MvcResult loginResult =
|
||||||
|
mockMvc.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(connectionRequestBody(wm))).andExpect(status().isOk()).andReturn();
|
||||||
|
|
||||||
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
|
mockMvc.perform(post(SEARCH_REQUEST).cookie(sessionCookie)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content(searchRequestBody(wm, "HOMEBOX")))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.content[0].title").value("MacBook Pro A1398"))
|
||||||
|
.andExpect(jsonPath("$.totalElements").value(1))
|
||||||
|
.andExpect(jsonPath("$.content[0].extraData.location.name")
|
||||||
|
.value("Server Schrank Ikea weiß"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnUnauthorizedWhenNoSession() throws Exception {
|
||||||
|
mockMvc.perform(post(SEARCH_REQUEST).contentType(MediaType.APPLICATION_JSON).content("""
|
||||||
|
{
|
||||||
|
"appUrl": "http://irrelevant",
|
||||||
|
"query": "Item",
|
||||||
|
"serviceType": "HOMEBOX",
|
||||||
|
"username": "irrelevant"
|
||||||
|
}
|
||||||
|
""")).andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnUnauthorizedWhenSessionHasNoConnectionForRequestedServiceType(
|
||||||
|
WireMockRuntimeInfo wm) throws Exception {
|
||||||
|
|
||||||
|
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
||||||
|
.willReturn(WireMock.okJson(VALID_HOMEBOX_LOGIN_RESPONSE)));
|
||||||
|
|
||||||
|
MvcResult loginResult =
|
||||||
|
mockMvc.perform(post(LOGIN_PATH).contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(connectionRequestBody(wm))).andExpect(status().isOk()).andReturn();
|
||||||
|
|
||||||
|
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
||||||
|
|
||||||
|
mockMvc.perform(
|
||||||
|
post(SEARCH_REQUEST).cookie(sessionCookie).contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(searchRequestBody(wm, "OTHER_SERVICETYPE")))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String searchRequestBody(WireMockRuntimeInfo wm, String serviceType) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"appUrl": "%s",
|
||||||
|
"query": "Item",
|
||||||
|
"serviceType": "%s",
|
||||||
|
"username": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(wm.getHttpBaseUrl(), serviceType, MOCK_USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String connectionRequestBody(WireMockRuntimeInfo wm) {
|
||||||
|
return """
|
||||||
|
{
|
||||||
|
"appUrl": "%s",
|
||||||
|
"serviceType": "HOMEBOX",
|
||||||
|
"username": "%s",
|
||||||
|
"password": "%s"
|
||||||
|
}
|
||||||
|
""".formatted(wm.getHttpBaseUrl(), MOCK_USER, MOCK_PASS);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import static com.vaessl.app.Mockdata.*;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class SearchResponseTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNullWhenExtraDataIsNull() {
|
||||||
|
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, null);
|
||||||
|
|
||||||
|
assertThat(response.getExtra(null)).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnNullWhenExtraDataKeyIsMissing() {
|
||||||
|
|
||||||
|
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
|
||||||
|
|
||||||
|
assertThat(response.getExtra("missing")).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExtraDataValue() {
|
||||||
|
|
||||||
|
SearchResponse response = new SearchResponse(MOCK_TITLE, MOCK_DESCRIPTION, Map.of("key", "value"));
|
||||||
|
|
||||||
|
assertThat(response.getExtra("key")).contains("value");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,7 +94,7 @@ The entire commit can be reviewed under following hash 43bbcece7a901e94021e10bca
|
|||||||
- DELETE /connections/{serviceType} — removes the specific key from the session. If no connections remain, the session is invalidated entirely.
|
- DELETE /connections/{serviceType} — removes the specific key from the session. If no connections remain, the session is invalidated entirely.
|
||||||
|
|
||||||
---
|
---
|
||||||
**connection/HomeBoxConnectionProvider.java (modified)**
|
**connection/HomeboxConnectionProvider.java (modified)**
|
||||||
|
|
||||||
Implements the new interface methods:
|
Implements the new interface methods:
|
||||||
- checkCredentials validates that username and password are present before touching the network.
|
- checkCredentials validates that username and password are present before touching the network.
|
||||||
|
|||||||
@@ -94,11 +94,11 @@ public interface ConnectionProvider {
|
|||||||
- `findUniqueConnectionEntry` — looks up an existing record to decide insert vs. update
|
- `findUniqueConnectionEntry` — looks up an existing record to decide insert vs. update
|
||||||
- `getTokenExpiry` — default returns `null` (no expiry); token-based providers override this so `ConnectionService` can compute the `connected` flag
|
- `getTokenExpiry` — default returns `null` (no expiry); token-based providers override this so `ConnectionService` can compute the `connected` flag
|
||||||
|
|
||||||
***HomeBoxConnectionProvider.java***
|
***HomeboxConnectionProvider.java***
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Component
|
@Component
|
||||||
public class HomeBoxConnectionProvider implements ConnectionProvider {
|
public class HomeboxConnectionProvider implements ConnectionProvider {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServiceType() { return "HOMEBOX"; }
|
public String getServiceType() { return "HOMEBOX"; }
|
||||||
|
|||||||
Reference in New Issue
Block a user