refactored to make use of the ServiceType enum throughout backend and frontend for typesafety
This commit is contained in:
@@ -2,5 +2,5 @@ package com.vaessl.app.connection;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
||||||
public record AuthResponse(String serviceType, Instant expiresAt) {
|
public record AuthResponse(ServiceType serviceType, Instant expiresAt) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,19 +53,24 @@ public class ConnectionController {
|
|||||||
Collections.list(session.getAttributeNames()).stream()
|
Collections.list(session.getAttributeNames()).stream()
|
||||||
.filter(k -> k.endsWith(SessionKeys.CONNECTION_ID_SUFFIX))
|
.filter(k -> k.endsWith(SessionKeys.CONNECTION_ID_SUFFIX))
|
||||||
.forEach(k -> {
|
.forEach(k -> {
|
||||||
String serviceType = k.replace(SessionKeys.CONNECTION_ID_SUFFIX, "");
|
String name = k.replace(SessionKeys.CONNECTION_ID_SUFFIX, "");
|
||||||
|
try {
|
||||||
|
ServiceType serviceType = ServiceType.valueOf(name);
|
||||||
Long id = (Long) session.getAttribute(k);
|
Long id = (Long) session.getAttribute(k);
|
||||||
ConnectionStatusResponse status =
|
ConnectionStatusResponse status =
|
||||||
connectionService.getConnectionStatus(serviceType, id);
|
connectionService.getConnectionStatus(serviceType, id);
|
||||||
if (status != null)
|
if (status != null)
|
||||||
statuses.add(status);
|
statuses.add(status);
|
||||||
|
} catch (IllegalArgumentException _) {
|
||||||
|
// stale session key from an old or removed service type — skip it
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ResponseEntity.ok(statuses);
|
return ResponseEntity.ok(statuses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/connections/{serviceType}")
|
@DeleteMapping("/connections/{serviceType}")
|
||||||
public ResponseEntity<Void> logout(@PathVariable("serviceType") String serviceType,
|
public ResponseEntity<Void> logout(@PathVariable("serviceType") ServiceType serviceType,
|
||||||
HttpServletRequest httpReq) {
|
HttpServletRequest httpReq) {
|
||||||
|
|
||||||
HttpSession session = httpReq.getSession(false);
|
HttpSession session = httpReq.getSession(false);
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ 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;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record ConnectionRequest(@NotBlank(message = "App URL is mandatory") String appUrl,
|
public record ConnectionRequest(@NotBlank(message = "App URL is mandatory") String appUrl,
|
||||||
@NotBlank(message = "Service type is mandatory") String serviceType,
|
@NotNull(message = "Service type is mandatory") ServiceType serviceType,
|
||||||
String username, String password, String apiKey,
|
String username, String password, String apiKey,
|
||||||
@JsonProperty(defaultValue = "false") Boolean stayLoggedIn) {
|
@JsonProperty(defaultValue = "false") Boolean stayLoggedIn) {
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ public record ConnectionRequest(@NotBlank(message = "App URL is mandatory") Stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionRequest(String appUrl, String serviceType, String username,
|
public ConnectionRequest(String appUrl, ServiceType serviceType, String username,
|
||||||
String password, Boolean stayLoggedIn) {
|
String password, Boolean stayLoggedIn) {
|
||||||
this(appUrl, serviceType, username, password, null, stayLoggedIn);
|
this(appUrl, serviceType, username, password, null, stayLoggedIn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import com.vaessl.app.exception.WrongServiceTypeException;
|
|||||||
@Service
|
@Service
|
||||||
public class ConnectionService {
|
public class ConnectionService {
|
||||||
|
|
||||||
private final Map<String, ConnectionProvider> providerRegistry;
|
private final Map<ServiceType, ConnectionProvider> providerRegistry;
|
||||||
|
|
||||||
private final ConnectionRepository cRepository;
|
private final ConnectionRepository cRepository;
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class ConnectionService {
|
|||||||
return new LoginResult(saved.getId(), response.expiresAt());
|
return new LoginResult(saved.getId(), response.expiresAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConnectionStatusResponse getConnectionStatus(String serviceType, Long connectionId) {
|
public ConnectionStatusResponse getConnectionStatus(ServiceType serviceType, Long connectionId) {
|
||||||
ConnectionEntity entity = cRepository.findById(connectionId).orElse(null);
|
ConnectionEntity entity = cRepository.findById(connectionId).orElse(null);
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
return null;
|
return null;
|
||||||
@@ -57,7 +57,7 @@ public class ConnectionService {
|
|||||||
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(), entity.getUsername(),
|
return new ConnectionStatusResponse(serviceType.name(), entity.getAppUrl(), entity.getUsername(),
|
||||||
expiresAt, connected);
|
expiresAt, connected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ public class HomeboxConnectionProvider implements ConnectionProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServiceType() {
|
public ServiceType getServiceType() {
|
||||||
return "HOMEBOX";
|
return ServiceType.HOMEBOX;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ public interface ServiceProvider {
|
|||||||
* Returns the service type key used to look up this provider in a registry, e.g.
|
* Returns the service type key used to look up this provider in a registry, e.g.
|
||||||
* {@code "HOMEBOX"}.
|
* {@code "HOMEBOX"}.
|
||||||
*/
|
*/
|
||||||
String getServiceType();
|
ServiceType getServiceType();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
package com.vaessl.app.connection;
|
package com.vaessl.app.connection;
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public enum ServiceType {
|
public enum ServiceType {
|
||||||
HOMEBOX("HOMEBOX");
|
HOMEBOX;
|
||||||
|
|
||||||
private final String value;
|
|
||||||
|
|
||||||
private ServiceType(String value) {
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public final class SessionKeys {
|
|||||||
|
|
||||||
private SessionKeys() {}
|
private SessionKeys() {}
|
||||||
|
|
||||||
public static String connectionId(String serviceType) {
|
public static String connectionId(ServiceType serviceType) {
|
||||||
return serviceType + CONNECTION_ID_SUFFIX;
|
return serviceType.name() + CONNECTION_ID_SUFFIX;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,20 +12,15 @@ import org.springframework.web.client.RestClient;
|
|||||||
import com.vaessl.app.connection.ConnectionEntity;
|
import com.vaessl.app.connection.ConnectionEntity;
|
||||||
import com.vaessl.app.connection.ConnectionRepository;
|
import com.vaessl.app.connection.ConnectionRepository;
|
||||||
import com.vaessl.app.connection.HomeboxEntity;
|
import com.vaessl.app.connection.HomeboxEntity;
|
||||||
|
import com.vaessl.app.connection.ServiceType;
|
||||||
import com.vaessl.app.exception.ConnectionNotFoundException;
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
import com.vaessl.app.exception.RemoteApiException;
|
import com.vaessl.app.exception.RemoteApiException;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import static com.vaessl.app.connection.Endpoint.*;
|
import static com.vaessl.app.connection.Endpoint.*;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class HomeboxSearchProvider implements SearchProvider {
|
public class HomeboxSearchProvider implements SearchProvider {
|
||||||
|
|
||||||
// TODO: Remove logger before merge
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(HomeboxSearchProvider.class);
|
|
||||||
|
|
||||||
private final RestClient.Builder restClientBuilder;
|
private final RestClient.Builder restClientBuilder;
|
||||||
|
|
||||||
private final ConnectionRepository cRepository;
|
private final ConnectionRepository cRepository;
|
||||||
@@ -37,8 +32,8 @@ public class HomeboxSearchProvider implements SearchProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServiceType() {
|
public ServiceType getServiceType() {
|
||||||
return "HOMEBOX";
|
return ServiceType.HOMEBOX;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -71,8 +66,6 @@ public class HomeboxSearchProvider implements SearchProvider {
|
|||||||
return new SearchResponse(id, title, description, extraSearchResponseData);
|
return new SearchResponse(id, title, description, extraSearchResponseData);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
log.info("Search results for query '{}': {}", request.query(), items);
|
|
||||||
|
|
||||||
return new PageImpl<>(items, pageable, hbResponse.total());
|
return new PageImpl<>(items, pageable, hbResponse.total());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.vaessl.app.search;
|
package com.vaessl.app.search;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ServiceType;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
public record SearchRequest(@NotBlank String appUrl, @NotBlank String username, String query,
|
||||||
@NotBlank String serviceType) {
|
@NotNull ServiceType serviceType) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import java.util.stream.Collectors;
|
|||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import com.vaessl.app.connection.ServiceType;
|
||||||
import com.vaessl.app.exception.WrongServiceTypeException;
|
import com.vaessl.app.exception.WrongServiceTypeException;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class SearchService {
|
public class SearchService {
|
||||||
|
|
||||||
private final Map<String, SearchProvider> providerRegistry;
|
private final Map<ServiceType, SearchProvider> providerRegistry;
|
||||||
|
|
||||||
public SearchService(List<SearchProvider> providers) {
|
public SearchService(List<SearchProvider> providers) {
|
||||||
this.providerRegistry = Map.copyOf(providers.stream()
|
this.providerRegistry = Map.copyOf(providers.stream()
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package com.vaessl.app;
|
package com.vaessl.app;
|
||||||
|
|
||||||
|
import com.vaessl.app.connection.ServiceType;
|
||||||
|
|
||||||
public final class Mockdata {
|
public final class Mockdata {
|
||||||
|
|
||||||
private Mockdata() {}
|
private Mockdata() {}
|
||||||
|
|
||||||
public static final String MOCK_URL = "http://localhost:1234";
|
public static final String MOCK_URL = "http://localhost:1234";
|
||||||
public static final String MOCK_SERVICE_TYPE = "SERVICE_TYPE";
|
public static final ServiceType MOCK_SERVICE_TYPE = ServiceType.HOMEBOX;
|
||||||
public static final String MOCK_USER = "user";
|
public static final String MOCK_USER = "user";
|
||||||
public static final String MOCK_PASS = "pw";
|
public static final String MOCK_PASS = "pw";
|
||||||
public static final String MOCK_ID = "item-1";
|
public static final String MOCK_ID = "item-1";
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class ConnectionControllerTest {
|
|||||||
private static final String LOGIN_PATH = LOGIN.getValue();
|
private static final String LOGIN_PATH = LOGIN.getValue();
|
||||||
private static final String STATUS_PATH = CONNECTION_STATUS.getValue();
|
private static final String STATUS_PATH = CONNECTION_STATUS.getValue();
|
||||||
private static final String LOGOUT_PATH = "/connections/HOMEBOX";
|
private static final String LOGOUT_PATH = "/connections/HOMEBOX";
|
||||||
|
private static final String SESSION = "SESSION";
|
||||||
|
|
||||||
private static final String VALID_HOMEBOX_RESPONSE = """
|
private static final String VALID_HOMEBOX_RESPONSE = """
|
||||||
{
|
{
|
||||||
@@ -66,11 +67,11 @@ class ConnectionControllerTest {
|
|||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk()).andReturn();
|
.andExpect(status().isOk()).andReturn();
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie(SESSION);
|
||||||
|
|
||||||
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).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.name()))
|
||||||
.andExpect(jsonPath("$[0].username").value(MOCK_USER))
|
.andExpect(jsonPath("$[0].username").value(MOCK_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));
|
||||||
@@ -87,10 +88,10 @@ class ConnectionControllerTest {
|
|||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk()).andReturn();
|
.andExpect(status().isOk()).andReturn();
|
||||||
|
|
||||||
Cookie sessionCookie = loginResult.getResponse().getCookie("SESSION");
|
Cookie sessionCookie = loginResult.getResponse().getCookie(SESSION);
|
||||||
|
|
||||||
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.getValue()))
|
.andExpect(jsonPath("$[0].serviceType").value(HOMEBOX.name()))
|
||||||
.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"));
|
||||||
@@ -106,7 +107,7 @@ class ConnectionControllerTest {
|
|||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk()).andReturn();
|
.andExpect(status().isOk()).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());
|
||||||
@@ -122,7 +123,7 @@ class ConnectionControllerTest {
|
|||||||
.content(connectionRequestBody(wm)))
|
.content(connectionRequestBody(wm)))
|
||||||
.andExpect(status().isOk()).andReturn();
|
.andExpect(status().isOk()).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)).andExpect(status().isOk())
|
mockMvc.perform(get(STATUS_PATH).cookie(sessionCookie)).andExpect(status().isOk())
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
|
|
||||||
import com.vaessl.app.exception.EmptyCredentialsException;
|
import com.vaessl.app.exception.EmptyCredentialsException;
|
||||||
import static com.vaessl.app.Mockdata.*;
|
import static com.vaessl.app.Mockdata.*;
|
||||||
|
import static com.vaessl.app.connection.ServiceType.HOMEBOX;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class HomeboxConnectionProviderTest {
|
class HomeboxConnectionProviderTest {
|
||||||
@@ -13,8 +14,9 @@ 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 checkCredentialsShouldThrowExceptionWhenFieldsAreMissing() {
|
||||||
ConnectionRequest request = new ConnectionRequest(MOCK_URL, "HOMEBOX", null, null, false);
|
ConnectionRequest request =
|
||||||
|
new ConnectionRequest(MOCK_URL, HOMEBOX, null, null, false);
|
||||||
|
|
||||||
EmptyCredentialsException exception = assertThrows(EmptyCredentialsException.class, () -> {
|
EmptyCredentialsException exception = assertThrows(EmptyCredentialsException.class, () -> {
|
||||||
provider.checkCredentials(request);
|
provider.checkCredentials(request);
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.boot.resttestclient.TestRestTemplate;
|
import org.springframework.boot.resttestclient.TestRestTemplate;
|
||||||
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
|
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
import com.github.tomakehurst.wiremock.http.Fault;
|
import com.github.tomakehurst.wiremock.http.Fault;
|
||||||
@@ -52,8 +56,8 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(okJson(okJsonHomeboxResponse)));
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(okJson(okJsonHomeboxResponse)));
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
connectionRequest(wm), String.class);
|
||||||
|
|
||||||
DocumentContext documentContext = JsonPath.parse(response.getBody());
|
DocumentContext documentContext = JsonPath.parse(response.getBody());
|
||||||
|
|
||||||
@@ -75,8 +79,8 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(unauthorized()));
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(unauthorized()));
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
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());
|
||||||
@@ -92,8 +96,8 @@ class HomeboxIntegrationTest {
|
|||||||
|
|
||||||
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(serviceUnavailable()));
|
stubFor(post(HOMEBOX_LOGIN.getValue()).willReturn(serviceUnavailable()));
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), connectionRequest(wm), String.class);
|
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());
|
||||||
@@ -109,11 +113,12 @@ 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 =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), badRequest, String.class);
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,10 +127,10 @@ class HomeboxIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldReturnBadRequestWhenHomeboxFieldsAreEmpty() {
|
void shouldReturnBadRequestWhenHomeboxFieldsAreEmpty() {
|
||||||
|
|
||||||
ConnectionRequest emtpyRequest = new ConnectionRequest("", "", "", "", false);
|
ConnectionRequest emtpyRequest = new ConnectionRequest("", null, "", "", false);
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), emtpyRequest, String.class);
|
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());
|
||||||
@@ -135,15 +140,20 @@ class HomeboxIntegrationTest {
|
|||||||
* Test the exception when there is an input for serviceType but it's unsupported.
|
* Test the exception when there is an input for serviceType but it's unsupported.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnWrongServiceTypeException(WireMockRuntimeInfo wm) {
|
void shouldReturnBadRequestWhenServiceTypeIsInvalid(WireMockRuntimeInfo wm) {
|
||||||
ConnectionRequest wrongServiceTypeReq = new ConnectionRequest(wm.getHttpBaseUrl(),
|
String body = """
|
||||||
"wrong-service-type", MOCK_USER, MOCK_PASS, false);
|
{"appUrl":"%s","serviceType":"wrong-service-type","username":"%s","password":"%s"}
|
||||||
|
"""
|
||||||
|
.formatted(wm.getHttpBaseUrl(), MOCK_USER, MOCK_PASS);
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
HttpHeaders headers = new HttpHeaders();
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), wrongServiceTypeReq, String.class);
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
HttpEntity<String> entity = new HttpEntity<>(body, headers);
|
||||||
|
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
ResponseEntity<String> response = restTemplate.exchange(LOGIN.getValue(),
|
||||||
assertThat(response.getBody()).contains(WRONG_SERVICE_TYPE.getMessage());
|
HttpMethod.POST, entity, String.class);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,8 +173,8 @@ class HomeboxIntegrationTest {
|
|||||||
restTemplate.postForEntity(LOGIN.getValue(), request, String.class);
|
restTemplate.postForEntity(LOGIN.getValue(), request, String.class);
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
ConnectionEntity dbEntry =
|
ConnectionEntity dbEntry = cRepository.findByAppUrlAndUsername(request.appUrl(),
|
||||||
cRepository.findByAppUrlAndUsername(request.appUrl(), request.username());
|
request.username());
|
||||||
|
|
||||||
assertThat(dbEntry).isNotNull();
|
assertThat(dbEntry).isNotNull();
|
||||||
assertThat(dbEntry.getAppUrl()).isEqualTo(request.appUrl());
|
assertThat(dbEntry.getAppUrl()).isEqualTo(request.appUrl());
|
||||||
@@ -173,17 +183,18 @@ class HomeboxIntegrationTest {
|
|||||||
if (dbEntry instanceof HomeboxEntity hbE) {
|
if (dbEntry instanceof HomeboxEntity hbE) {
|
||||||
assertThat(hbE.getToken()).isEqualTo("fake-jwt-token");
|
assertThat(hbE.getToken()).isEqualTo("fake-jwt-token");
|
||||||
assertThat(hbE.getAttachmentToken()).isEqualTo("fake-attach");
|
assertThat(hbE.getAttachmentToken()).isEqualTo("fake-attach");
|
||||||
assertThat(hbE.getExpiresAt().toString()).hasToString("2026-04-26T02:23:13Z");
|
assertThat(hbE.getExpiresAt().toString())
|
||||||
|
.hasToString("2026-04-26T02:23:13Z");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnEmptyCredentialsExceptionWhenCredsAreMissing(WireMockRuntimeInfo wm) {
|
void shouldReturnEmptyCredentialsExceptionWhenCredsAreMissing(WireMockRuntimeInfo wm) {
|
||||||
ConnectionRequest missingCredentials = new ConnectionRequest(wm.getHttpBaseUrl(),
|
ConnectionRequest missingCredentials = new ConnectionRequest(wm.getHttpBaseUrl(),
|
||||||
HOMEBOX.getValue(), MOCK_USER, null, false);
|
HOMEBOX, MOCK_USER, null, false);
|
||||||
|
|
||||||
ResponseEntity<String> response =
|
ResponseEntity<String> response = restTemplate.postForEntity(LOGIN.getValue(),
|
||||||
restTemplate.postForEntity(LOGIN.getValue(), missingCredentials, String.class);
|
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());
|
||||||
@@ -196,7 +207,7 @@ class HomeboxIntegrationTest {
|
|||||||
* @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(), MOCK_USER, MOCK_PASS,
|
return new ConnectionRequest(wm.getHttpBaseUrl(), HOMEBOX, MOCK_USER, MOCK_PASS,
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import org.springframework.data.domain.PageRequest;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import com.vaessl.app.connection.ConnectionRepository;
|
import com.vaessl.app.connection.ConnectionRepository;
|
||||||
import com.vaessl.app.exception.ConnectionNotFoundException;
|
import com.vaessl.app.exception.ConnectionNotFoundException;
|
||||||
|
import static com.vaessl.app.connection.ServiceType.HOMEBOX;
|
||||||
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class HomeboxSearchProviderTest {
|
class HomeboxSearchProviderTest {
|
||||||
@@ -27,7 +29,7 @@ class HomeboxSearchProviderTest {
|
|||||||
|
|
||||||
when(mockRepo.findByAppUrlAndUsername(MOCK_URL, MOCK_USER)).thenReturn(null);
|
when(mockRepo.findByAppUrlAndUsername(MOCK_URL, MOCK_USER)).thenReturn(null);
|
||||||
|
|
||||||
SearchRequest request = new SearchRequest(MOCK_URL, MOCK_USER, "test query", "HOMEBOX");
|
SearchRequest request = new SearchRequest(MOCK_URL, MOCK_USER, "test query", HOMEBOX);
|
||||||
Pageable pageable = PageRequest.of(0, 10);
|
Pageable pageable = PageRequest.of(0, 10);
|
||||||
assertThrows(ConnectionNotFoundException.class,
|
assertThrows(ConnectionNotFoundException.class,
|
||||||
() -> provider.getSearchResults(request, pageable));
|
() -> provider.getSearchResults(request, pageable));
|
||||||
|
|||||||
@@ -110,25 +110,21 @@ class SearchControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReturnUnauthorizedWhenSessionHasNoConnectionForRequestedServiceType(
|
void shouldReturnBadRequestWhenServiceTypeIsInvalid() throws Exception {
|
||||||
WireMockRuntimeInfo wm) throws Exception {
|
mockMvc.perform(post(SEARCH_REQUEST).contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(searchRequestBody("INVALID_SERVICETYPE")))
|
||||||
WireMock.stubFor(WireMock.post(HOMEBOX_LOGIN.getValue())
|
.andExpect(status().isBadRequest());
|
||||||
.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) {
|
private String searchRequestBody(WireMockRuntimeInfo wm, String serviceType) {
|
||||||
|
return searchRequestBody(wm.getHttpBaseUrl(), serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String searchRequestBody(String serviceType) {
|
||||||
|
return searchRequestBody("http://irrelevant", serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String searchRequestBody(String appUrl, String serviceType) {
|
||||||
return """
|
return """
|
||||||
{
|
{
|
||||||
"appUrl": "%s",
|
"appUrl": "%s",
|
||||||
@@ -136,7 +132,7 @@ class SearchControllerTest {
|
|||||||
"serviceType": "%s",
|
"serviceType": "%s",
|
||||||
"username": "%s"
|
"username": "%s"
|
||||||
}
|
}
|
||||||
""".formatted(wm.getHttpBaseUrl(), serviceType, MOCK_USER);
|
""".formatted(appUrl, serviceType, MOCK_USER);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String connectionRequestBody(WireMockRuntimeInfo wm) {
|
private String connectionRequestBody(WireMockRuntimeInfo wm) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { apiFetch } from './client'
|
import { apiFetch } from './client'
|
||||||
import type { AuthResponse, ConnectionStatus, LoginRequest } from '../types/connection'
|
import type { AuthResponse, ConnectionStatus, LoginRequest, ServiceType } from '../types/connection'
|
||||||
|
|
||||||
export const login = (req: LoginRequest) =>
|
export const login = (req: LoginRequest) =>
|
||||||
apiFetch<AuthResponse>('/login', { method: 'POST', body: JSON.stringify(req) })
|
apiFetch<AuthResponse>('/login', { method: 'POST', body: JSON.stringify(req) })
|
||||||
@@ -7,5 +7,5 @@ export const login = (req: LoginRequest) =>
|
|||||||
export const getStatuses = () =>
|
export const getStatuses = () =>
|
||||||
apiFetch<ConnectionStatus[]>('/connections/status')
|
apiFetch<ConnectionStatus[]>('/connections/status')
|
||||||
|
|
||||||
export const logout = (serviceType: string) =>
|
export const logout = (serviceType: ServiceType) =>
|
||||||
apiFetch<void>(`/connections/${serviceType}`, { method: 'DELETE' })
|
apiFetch<void>(`/connections/${serviceType}`, { method: 'DELETE' })
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
|
import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
|
||||||
import { login } from '../../api/connections'
|
import { login } from '../../api/connections'
|
||||||
import type { LoginRequest } from '../../types/connection'
|
import type { LoginRequest, ServiceType } from '../../types/connection'
|
||||||
import '../ui/Modal.scss'
|
import '../ui/Modal.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceType: string
|
serviceType: ServiceType
|
||||||
label: string
|
label: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { getStatuses, logout } from "../../api/connections"
|
import { getStatuses, logout } from "../../api/connections"
|
||||||
import type { ConnectionStatus } from "../../types/connection"
|
import { ServiceType, type ConnectionStatus } from "../../types/connection"
|
||||||
import { ServiceCard } from "./ServiceCard"
|
import { ServiceCard } from "./ServiceCard"
|
||||||
import { ConnectModal } from "./ConnectModal"
|
import { ConnectModal } from "./ConnectModal"
|
||||||
import "./Dashboard.scss"
|
import "./Dashboard.scss"
|
||||||
import { SearchModal } from "../search/SearchModal"
|
import { SearchModal } from "../search/SearchModal"
|
||||||
|
|
||||||
const SERVICES = [
|
const SERVICES = [
|
||||||
{ serviceType: 'HOMEBOX', label: 'Homebox', icon: '📦' },
|
{ serviceType: ServiceType.HOMEBOX, label: 'Homebox', icon: '📦' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export function Dashboard() {
|
export function Dashboard() {
|
||||||
@@ -21,7 +21,7 @@ export function Dashboard() {
|
|||||||
|
|
||||||
useEffect(() => { refresh() }, [])
|
useEffect(() => { refresh() }, [])
|
||||||
|
|
||||||
const handleDisconnect = (serviceType: string) => {
|
const handleDisconnect = (serviceType: ServiceType) => {
|
||||||
logout(serviceType).then(refresh).catch(() => { })
|
logout(serviceType).then(refresh).catch(() => { })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { ConnectionStatus } from '../../types/connection'
|
import type { ConnectionStatus, ServiceType } from '../../types/connection'
|
||||||
import { ActionButton } from '../ui/ActionButton'
|
import { ActionButton } from '../ui/ActionButton'
|
||||||
import './ServiceCard.scss'
|
import './ServiceCard.scss'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceType: string
|
serviceType: ServiceType
|
||||||
label: string
|
label: string
|
||||||
icon: string
|
icon: string
|
||||||
status: ConnectionStatus | null
|
status: ConnectionStatus | null
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { useEffect, useRef, useState, type SyntheticEvent } from 'react'
|
|||||||
import '../ui/Modal.scss'
|
import '../ui/Modal.scss'
|
||||||
import { type PagedSearchResponse, type SearchResponse, type SearchRequest } from '../../types/search'
|
import { type PagedSearchResponse, type SearchResponse, type SearchRequest } from '../../types/search'
|
||||||
import { search } from '../../api/searches'
|
import { search } from '../../api/searches'
|
||||||
|
import type { ServiceType } from '../../types/connection'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serviceType: string
|
serviceType: ServiceType
|
||||||
label: string
|
label: string
|
||||||
appUrl: string
|
appUrl: string
|
||||||
username: string
|
username: string
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
|
export const ServiceType = {
|
||||||
|
HOMEBOX: "HOMEBOX",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ServiceType = (typeof ServiceType)[keyof typeof ServiceType];
|
||||||
|
|
||||||
export interface LoginRequest {
|
export interface LoginRequest {
|
||||||
appUrl: string
|
appUrl: string;
|
||||||
serviceType: string
|
serviceType: ServiceType;
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
stayLoggedIn: boolean
|
stayLoggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
serviceType: string
|
serviceType: ServiceType;
|
||||||
expiresAt: string
|
expiresAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionStatus {
|
export interface ConnectionStatus {
|
||||||
serviceType: string
|
serviceType: ServiceType;
|
||||||
appUrl: string
|
appUrl: string;
|
||||||
username: string
|
username: string;
|
||||||
expiresAt: string | null
|
expiresAt: string | null;
|
||||||
connected: boolean
|
connected: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
|
import type { ServiceType } from "./connection";
|
||||||
|
|
||||||
export interface SearchRequest {
|
export interface SearchRequest {
|
||||||
appUrl: string;
|
appUrl: string
|
||||||
serviceType: string;
|
serviceType: ServiceType
|
||||||
username: string;
|
username: string
|
||||||
query: string | null;
|
query: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResponse {
|
export interface SearchResponse {
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
description: string | null;
|
description: string | null
|
||||||
extraData: Record<string, unknown>;
|
extraData: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PagedSearchResponse<T> {
|
export interface PagedSearchResponse<T> {
|
||||||
content: T[];
|
content: T[]
|
||||||
page: number;
|
page: number
|
||||||
pageSize: number;
|
pageSize: number
|
||||||
totalElements: number;
|
totalElements: number
|
||||||
first: boolean;
|
first: boolean
|
||||||
last: boolean;
|
last: boolean
|
||||||
sort: string;
|
sort: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user