refactored connection classes to be more generic and accept credentials of different apps.

This commit is contained in:
2026-04-03 02:58:34 +02:00
parent ab1d7e68f5
commit 0169cf04b6
7 changed files with 116 additions and 30 deletions
@@ -0,0 +1,11 @@
package com.vaessl.app.connection;
import com.vaessl.app.dto.ConnectionRequest;
import com.vaessl.app.dto.ConnectionResponse;
public interface ConnectionProvider {
String getServiceType();
ConnectionResponse authenticate (ConnectionRequest connectionRequest);
}
@@ -1,7 +1,10 @@
package com.vaessl.app.connection; package com.vaessl.app.connection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import com.vaessl.app.dto.ConnectionRequest; import com.vaessl.app.dto.ConnectionRequest;
import com.vaessl.app.dto.ConnectionResponse; import com.vaessl.app.dto.ConnectionResponse;
@@ -9,20 +12,20 @@ import com.vaessl.app.dto.ConnectionResponse;
@Service @Service
public class ConnectionService { public class ConnectionService {
private final RestClient.Builder restClientBuilder; private final Map<String, ConnectionProvider> providerRegistry;
public ConnectionService(RestClient.Builder restClientBuilder) { public ConnectionService(List<ConnectionProvider> providers) {
this.restClientBuilder = restClientBuilder; this.providerRegistry = providers.stream()
.collect(Collectors.toMap(ConnectionProvider::getServiceType, p -> p));
} }
public ConnectionResponse login(ConnectionRequest request) { public ConnectionResponse login(ConnectionRequest request) {
// TODO: Look into Map<String, RestClient> to cache restclient requests. ConnectionProvider provider = providerRegistry.get(request.serviceType().toUpperCase());
return restClientBuilder.baseUrl(request.appUrl())
.build() if (provider == null) {
.post() throw new IllegalArgumentException("Unknown provider: " + request.serviceType());
.uri("/api/v1/users/login") }
.body(request)
.retrieve() return provider.authenticate(request);
.body(ConnectionResponse.class);
} }
} }
@@ -0,0 +1,14 @@
package com.vaessl.app.connection;
import lombok.Getter;
@Getter
public enum Endpoints {
HOMEBOX_LOGIN("/api/v1/users/login");
private final String endpoint;
Endpoints(String endpoint){
this.endpoint = endpoint;
}
}
@@ -0,0 +1,42 @@
package com.vaessl.app.connection;
import java.util.Map;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
import com.vaessl.app.dto.ConnectionRequest;
import com.vaessl.app.dto.ConnectionResponse;
import static com.vaessl.app.connection.Endpoints.*;
@Component
public class HomeBoxConnectionProvider implements ConnectionProvider {
private final RestClient.Builder restClientBuilder;
public HomeBoxConnectionProvider(RestClient.Builder restClientBuilder) {
this.restClientBuilder = restClientBuilder;
}
@Override
public String getServiceType() {
return "HOMEBOX";
}
@Override
public ConnectionResponse authenticate(ConnectionRequest connectionRequest) {
Map<String, Object> homeboxPayload = Map.of("username", connectionRequest.credentials().get("username"),
"password", connectionRequest.credentials().get("password"), "stayLoggedIn",
connectionRequest.stayLoggedIn());
return restClientBuilder.baseUrl(connectionRequest.appUrl())
.build()
.post()
.uri(HOMEBOX_LOGIN.getEndpoint())
.body(homeboxPayload)
.retrieve()
.body(ConnectionResponse.class);
}
}
@@ -1,9 +1,13 @@
package com.vaessl.app.dto; package com.vaessl.app.dto;
import java.util.Map;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
public record ConnectionRequest( public record ConnectionRequest(
@NotBlank(message = "App URL is mandatory") String appUrl, @NotBlank(message = "App URL is mandatory") String appUrl,
@NotBlank(message = "Username is mandatory") String username, @NotBlank String serviceType,
@NotBlank(message = "Password is mandatory") String password) { @NotEmpty Map <String, String> credentials,
boolean stayLoggedIn) {
} }
@@ -6,7 +6,6 @@ import lombok.Getter;
@Getter @Getter
public enum ErrorMessages { public enum ErrorMessages {
BAD_REQUEST_EMPTY_FIELDS(HttpStatus.BAD_REQUEST, "Fields must not be empty."), UNAUTHORIZED_WRONG_LOGIN( BAD_REQUEST_EMPTY_FIELDS(HttpStatus.BAD_REQUEST, "Fields must not be empty."), UNAUTHORIZED_WRONG_LOGIN(
HttpStatus.UNAUTHORIZED, "Invalid username or password."), SERVICE_UNAVAILABLE_UNREACHABLE_URL( HttpStatus.UNAUTHORIZED, "Invalid username or password."), SERVICE_UNAVAILABLE_UNREACHABLE_URL(
HttpStatus.SERVICE_UNAVAILABLE, "The target URL is unreachable."), SERVER_ERROR_GENERAL( HttpStatus.SERVICE_UNAVAILABLE, "The target URL is unreachable."), SERVER_ERROR_GENERAL(
@@ -16,25 +16,27 @@ import com.jayway.jsonpath.JsonPath;
import com.vaessl.app.dto.ConnectionRequest; import com.vaessl.app.dto.ConnectionRequest;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.vaessl.app.connection.Endpoints.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate @AutoConfigureTestRestTemplate
@WireMockTest @WireMockTest
public class ConnectionIntegrationTest { public class HomeboxIntegrationTest {
@Autowired @Autowired
TestRestTemplate restTemplate; TestRestTemplate restTemplate;
private static final String API_LOGIN = "/api/v1/users/login";
/** /**
* Returns Token and status code OK when login is successful. * Returns Token and status code OK when login is successful.
*/ */
@Test @Test
void shouldReturnTokenAndStatusOkWhenCredentialsAreValid(WireMockRuntimeInfo wm) { void shouldReturnTokenAndStatusOkWhenHomeboxCredentialsAreValid(WireMockRuntimeInfo wm) {
stubFor(post(API_LOGIN) stubFor(post(HOMEBOX_LOGIN.getEndpoint())
.willReturn(okJson(""" .willReturn(okJson("""
{ {
"token": "fake-jwt-token", "token": "fake-jwt-token",
@@ -64,7 +66,7 @@ public class ConnectionIntegrationTest {
@Test @Test
void shouldFailToConnectWhenHomeboxReturnsUnauthorized(WireMockRuntimeInfo wm) { void shouldFailToConnectWhenHomeboxReturnsUnauthorized(WireMockRuntimeInfo wm) {
stubFor(post(API_LOGIN).willReturn(unauthorized())); stubFor(post(HOMEBOX_LOGIN.getEndpoint()).willReturn(unauthorized()));
ResponseEntity<String> response = restTemplate.postForEntity("/login", connectionRequest(wm), String.class); ResponseEntity<String> response = restTemplate.postForEntity("/login", connectionRequest(wm), String.class);
@@ -78,7 +80,7 @@ public class ConnectionIntegrationTest {
@Test @Test
void shouldFailToConnectWhenHomeboxReturnsServiceUnavailable(WireMockRuntimeInfo wm) { void shouldFailToConnectWhenHomeboxReturnsServiceUnavailable(WireMockRuntimeInfo wm) {
stubFor(post(API_LOGIN).willReturn(serviceUnavailable())); stubFor(post(HOMEBOX_LOGIN.getEndpoint()).willReturn(serviceUnavailable()));
ResponseEntity<String> response = restTemplate.postForEntity("/login", connectionRequest(wm), String.class); ResponseEntity<String> response = restTemplate.postForEntity("/login", connectionRequest(wm), String.class);
@@ -90,15 +92,18 @@ public class ConnectionIntegrationTest {
* Checks when the service is unavailable or the app URL is wrong. * Checks when the service is unavailable or the app URL is wrong.
*/ */
@Test @Test
void shouldReturnServiceUnavailableWhenUrlIsCompletelyWrong() { void shouldReturnServiceUnavailableWhenHomeboxUrlIsWrong() {
ConnectionRequest badRequest = new ConnectionRequest( ConnectionRequest badRequest = new ConnectionRequest(
"http://localhost:1234", "http://localhost:1234",
"user", "HOMEBOX",
"pass"); Map.of("username", "myUser", "password", "myPass"),
false);
ResponseEntity<String> response = restTemplate.postForEntity("/login", badRequest, String.class); ResponseEntity<String> response = restTemplate.postForEntity("/login", badRequest, String.class);
System.out.println("RESPONSE: " + response.getBody());
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
assertThat(response.getBody()).contains("The target URL is unreachable."); assertThat(response.getBody()).contains("The target URL is unreachable.");
} }
@@ -107,9 +112,9 @@ public class ConnectionIntegrationTest {
* Checks if any login fields are empty since all of them are mandatory. * Checks if any login fields are empty since all of them are mandatory.
*/ */
@Test @Test
void shouldReturnBadRequestWhenFieldsAreEmpty() { void shouldReturnBadRequestWhenHomeboxFieldsAreEmpty() {
ConnectionRequest emtpyRequest = new ConnectionRequest("", "", ""); ConnectionRequest emtpyRequest = new ConnectionRequest("", "", Map.of(), false);
ResponseEntity<String> response = restTemplate.postForEntity("/login", emtpyRequest, String.class); ResponseEntity<String> response = restTemplate.postForEntity("/login", emtpyRequest, String.class);
@@ -117,7 +122,15 @@ public class ConnectionIntegrationTest {
assertThat(response.getBody()).contains("Fields must not be empty."); assertThat(response.getBody()).contains("Fields must not be empty.");
} }
private ConnectionRequest connectionRequest(WireMockRuntimeInfo wireMockRuntimeInfo) { /**
return new ConnectionRequest(wireMockRuntimeInfo.getHttpBaseUrl(), "username", "password"); * Creates a valid connection request with a mock Api throuh
* WireMockRuntimeInfo.
*
* @param wm
* @return a mock api connection request.
*/
private ConnectionRequest connectionRequest(WireMockRuntimeInfo wm) {
return new ConnectionRequest(wm.getHttpBaseUrl(), "HOMEBOX", Map.of("username", "admin", "password", "pw"),
false);
} }
} }