From 0169cf04b68b81c452d7e7c4897bb15d31575405 Mon Sep 17 00:00:00 2001 From: kasun Date: Fri, 3 Apr 2026 02:58:34 +0200 Subject: [PATCH] refactored connection classes to be more generic and accept credentials of different apps. --- .../app/connection/ConnectionProvider.java | 11 +++++ .../app/connection/ConnectionService.java | 27 ++++++------ .../com/vaessl/app/connection/Endpoints.java | 14 +++++++ .../connection/HomeBoxConnectionProvider.java | 42 +++++++++++++++++++ .../com/vaessl/app/dto/ConnectionRequest.java | 10 +++-- .../vaessl/app/exception/ErrorMessages.java | 1 - ...nTest.java => HomeboxIntegrationTest.java} | 41 +++++++++++------- 7 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 backend/src/main/java/com/vaessl/app/connection/ConnectionProvider.java create mode 100644 backend/src/main/java/com/vaessl/app/connection/Endpoints.java create mode 100644 backend/src/main/java/com/vaessl/app/connection/HomeBoxConnectionProvider.java rename backend/src/test/java/com/vaessl/app/connection/{ConnectionIntegrationTest.java => HomeboxIntegrationTest.java} (78%) diff --git a/backend/src/main/java/com/vaessl/app/connection/ConnectionProvider.java b/backend/src/main/java/com/vaessl/app/connection/ConnectionProvider.java new file mode 100644 index 0000000..80b4e8e --- /dev/null +++ b/backend/src/main/java/com/vaessl/app/connection/ConnectionProvider.java @@ -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); +} diff --git a/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java b/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java index 882f369..7a98f9f 100644 --- a/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java +++ b/backend/src/main/java/com/vaessl/app/connection/ConnectionService.java @@ -1,7 +1,10 @@ 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.web.client.RestClient; import com.vaessl.app.dto.ConnectionRequest; import com.vaessl.app.dto.ConnectionResponse; @@ -9,20 +12,20 @@ import com.vaessl.app.dto.ConnectionResponse; @Service public class ConnectionService { - private final RestClient.Builder restClientBuilder; + private final Map providerRegistry; - public ConnectionService(RestClient.Builder restClientBuilder) { - this.restClientBuilder = restClientBuilder; + public ConnectionService(List providers) { + this.providerRegistry = providers.stream() + .collect(Collectors.toMap(ConnectionProvider::getServiceType, p -> p)); } public ConnectionResponse login(ConnectionRequest request) { - // TODO: Look into Map to cache restclient requests. - return restClientBuilder.baseUrl(request.appUrl()) - .build() - .post() - .uri("/api/v1/users/login") - .body(request) - .retrieve() - .body(ConnectionResponse.class); + ConnectionProvider provider = providerRegistry.get(request.serviceType().toUpperCase()); + + if (provider == null) { + throw new IllegalArgumentException("Unknown provider: " + request.serviceType()); + } + + return provider.authenticate(request); } } \ No newline at end of file diff --git a/backend/src/main/java/com/vaessl/app/connection/Endpoints.java b/backend/src/main/java/com/vaessl/app/connection/Endpoints.java new file mode 100644 index 0000000..9f9711c --- /dev/null +++ b/backend/src/main/java/com/vaessl/app/connection/Endpoints.java @@ -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; + } +} diff --git a/backend/src/main/java/com/vaessl/app/connection/HomeBoxConnectionProvider.java b/backend/src/main/java/com/vaessl/app/connection/HomeBoxConnectionProvider.java new file mode 100644 index 0000000..aff1466 --- /dev/null +++ b/backend/src/main/java/com/vaessl/app/connection/HomeBoxConnectionProvider.java @@ -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 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); + } + +} diff --git a/backend/src/main/java/com/vaessl/app/dto/ConnectionRequest.java b/backend/src/main/java/com/vaessl/app/dto/ConnectionRequest.java index e9981a8..d01b63f 100644 --- a/backend/src/main/java/com/vaessl/app/dto/ConnectionRequest.java +++ b/backend/src/main/java/com/vaessl/app/dto/ConnectionRequest.java @@ -1,9 +1,13 @@ package com.vaessl.app.dto; +import java.util.Map; + import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; public record ConnectionRequest( - @NotBlank(message = "App URL is mandatory") String appUrl, - @NotBlank(message = "Username is mandatory") String username, - @NotBlank(message = "Password is mandatory") String password) { + @NotBlank(message = "App URL is mandatory") String appUrl, + @NotBlank String serviceType, + @NotEmpty Map credentials, + boolean stayLoggedIn) { } diff --git a/backend/src/main/java/com/vaessl/app/exception/ErrorMessages.java b/backend/src/main/java/com/vaessl/app/exception/ErrorMessages.java index 7e39eb7..7c77dab 100644 --- a/backend/src/main/java/com/vaessl/app/exception/ErrorMessages.java +++ b/backend/src/main/java/com/vaessl/app/exception/ErrorMessages.java @@ -6,7 +6,6 @@ import lombok.Getter; @Getter public enum ErrorMessages { - 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.SERVICE_UNAVAILABLE, "The target URL is unreachable."), SERVER_ERROR_GENERAL( diff --git a/backend/src/test/java/com/vaessl/app/connection/ConnectionIntegrationTest.java b/backend/src/test/java/com/vaessl/app/connection/HomeboxIntegrationTest.java similarity index 78% rename from backend/src/test/java/com/vaessl/app/connection/ConnectionIntegrationTest.java rename to backend/src/test/java/com/vaessl/app/connection/HomeboxIntegrationTest.java index 64d5186..f205159 100644 --- a/backend/src/test/java/com/vaessl/app/connection/ConnectionIntegrationTest.java +++ b/backend/src/test/java/com/vaessl/app/connection/HomeboxIntegrationTest.java @@ -16,25 +16,27 @@ import com.jayway.jsonpath.JsonPath; import com.vaessl.app.dto.ConnectionRequest; import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.vaessl.app.connection.Endpoints.*; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestRestTemplate @WireMockTest -public class ConnectionIntegrationTest { +public class HomeboxIntegrationTest { @Autowired TestRestTemplate restTemplate; - private static final String API_LOGIN = "/api/v1/users/login"; - /** * Returns Token and status code OK when login is successful. */ @Test - void shouldReturnTokenAndStatusOkWhenCredentialsAreValid(WireMockRuntimeInfo wm) { + void shouldReturnTokenAndStatusOkWhenHomeboxCredentialsAreValid(WireMockRuntimeInfo wm) { - stubFor(post(API_LOGIN) + stubFor(post(HOMEBOX_LOGIN.getEndpoint()) .willReturn(okJson(""" { "token": "fake-jwt-token", @@ -64,7 +66,7 @@ public class ConnectionIntegrationTest { @Test void shouldFailToConnectWhenHomeboxReturnsUnauthorized(WireMockRuntimeInfo wm) { - stubFor(post(API_LOGIN).willReturn(unauthorized())); + stubFor(post(HOMEBOX_LOGIN.getEndpoint()).willReturn(unauthorized())); ResponseEntity response = restTemplate.postForEntity("/login", connectionRequest(wm), String.class); @@ -78,7 +80,7 @@ public class ConnectionIntegrationTest { @Test void shouldFailToConnectWhenHomeboxReturnsServiceUnavailable(WireMockRuntimeInfo wm) { - stubFor(post(API_LOGIN).willReturn(serviceUnavailable())); + stubFor(post(HOMEBOX_LOGIN.getEndpoint()).willReturn(serviceUnavailable())); ResponseEntity 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. */ @Test - void shouldReturnServiceUnavailableWhenUrlIsCompletelyWrong() { + void shouldReturnServiceUnavailableWhenHomeboxUrlIsWrong() { ConnectionRequest badRequest = new ConnectionRequest( "http://localhost:1234", - "user", - "pass"); + "HOMEBOX", + Map.of("username", "myUser", "password", "myPass"), + false); ResponseEntity response = restTemplate.postForEntity("/login", badRequest, String.class); + System.out.println("RESPONSE: " + response.getBody()); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE); 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. */ @Test - void shouldReturnBadRequestWhenFieldsAreEmpty() { + void shouldReturnBadRequestWhenHomeboxFieldsAreEmpty() { - ConnectionRequest emtpyRequest = new ConnectionRequest("", "", ""); + ConnectionRequest emtpyRequest = new ConnectionRequest("", "", Map.of(), false); ResponseEntity response = restTemplate.postForEntity("/login", emtpyRequest, String.class); @@ -117,7 +122,15 @@ public class ConnectionIntegrationTest { 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); } }