Skip to content

Commit

Permalink
Add test for sync
Browse files Browse the repository at this point in the history
  • Loading branch information
mucsi96 committed Jul 13, 2023
1 parent 7e8cda9 commit 4801595
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 39 deletions.
5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Add test for sync
- Add test for weight get
- Add test for access token refresh
- Add test for sync forbidden
- Split Weight controller into Weigth controller and withings controller
- Adjust the UI
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@ControllerAdvice
public class AppControllerAdvice {
@ExceptionHandler(ClientAuthorizationRequiredException.class)
public ResponseEntity<RepresentationModel> handleClientAuthorizationRequired(
public ResponseEntity<RepresentationModel<?>> handleClientAuthorizationRequired(
ClientAuthorizationRequiredException ex) {
String oauth2LoginUrl = ServletUriComponentsBuilder.fromCurrentServletMapping().path(
OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package mucsi96.traininglog.configuration;
package mucsi96.traininglog.core;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -14,7 +14,6 @@
import org.springframework.security.web.SecurityFilterChain;

import io.github.mucsi96.kubetools.security.KubetoolsSecurityConfigurer;
import mucsi96.traininglog.core.RedirectToHomeRequestCache;
import mucsi96.traininglog.oauth.AccessTokenResponseClient;
import mucsi96.traininglog.oauth.AuthorizedClientManager;
import mucsi96.traininglog.oauth.RefreshTokenResponseClient;
Expand Down
5 changes: 5 additions & 0 deletions server/src/main/java/mucsi96/traininglog/weight/Weight.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;

@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor(access= AccessLevel.PRIVATE, force=true)
public class Weight {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package mucsi96.traininglog.withings;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import lombok.Data;

@Data
@Configuration
@ConfigurationProperties(prefix = "withings")
public class WithingsConfiguration {
private WithingsApiConfiguration api;

@Data
public static class WithingsApiConfiguration {
private String uri;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package mucsi96.traininglog.withings;

import java.time.Instant;
import java.util.Calendar;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;

Expand All @@ -13,6 +16,7 @@
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import lombok.RequiredArgsConstructor;
import mucsi96.traininglog.weight.Weight;
import mucsi96.traininglog.withings.data.GetMeasureResponse;
import mucsi96.traininglog.withings.data.GetMeasureResponseBody;
Expand All @@ -21,30 +25,22 @@
import mucsi96.traininglog.withings.oauth.WithingsClient;

@Service
@RequiredArgsConstructor
public class WithingsService {

int getStartDate() {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
return (int) (cal.getTimeInMillis() / 1000);
}

int getEndDate() {
Calendar cal = Calendar.getInstance();
return (int) (cal.getTimeInMillis() / 1000);
}
private final WithingsConfiguration withingsConfiguration;

private String getMeasureUrl() {
long startTime = LocalDateTime.of(LocalDate.now(), LocalTime.MIN).toInstant(ZoneOffset.UTC).getEpochSecond();
long endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX).toInstant(ZoneOffset.UTC).getEpochSecond();
return UriComponentsBuilder
.fromHttpUrl("https://wbsapi.withings.net")
.fromHttpUrl(withingsConfiguration.getApi().getUri())
.path("/measure")
.queryParam("action", "getmeas")
.queryParam("meastype", 1)
.queryParam("category", 1)
.queryParam("startdate", getStartDate())
.queryParam("enddate", getEndDate())
.queryParam("startdate", startTime)
.queryParam("enddate", endTime)
.build()
.encode()
.toUriString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{"properties": [{
"name": "withings",
"type": "mucsi96.traininglog.withings.WithingsConfiguration",
"description": "Configuration for Withings API"
}]}
3 changes: 3 additions & 0 deletions server/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ logging:
springdoc:
swagger-ui:
path: /
withings:
api:
uri: https://wbsapi.withings.net
102 changes: 91 additions & 11 deletions server/src/test/java/mucsi96/traininglog/WeightControllerTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Optional;

Expand Down Expand Up @@ -36,44 +41,63 @@
import lombok.RequiredArgsConstructor;
import mucsi96.traininglog.model.TestAuthorizedClient;
import mucsi96.traininglog.repository.TestAuthorizedClientRepository;
import mucsi96.traininglog.weight.Weight;
import mucsi96.traininglog.weight.WeightRepository;
import mucsi96.traininglog.withings.oauth.WithingsClient;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@RequiredArgsConstructor
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
public class WeightControllerTests extends BaseIntegrationTest {

// OAuth2AuthorizationCodeAuthenticationProvider

private final MockMvc mockMvc;
private final TestAuthorizedClientRepository authorizedClientRepository;
private final WeightRepository weightRepository;

@LocalServerPort
private int port;

@RegisterExtension
static WireMockExtension withingsServer = WireMockExtension.newInstance()
static WireMockExtension mockWithingsServer = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort())
.build();

@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {

registry.add("spring.security.oauth2.client.provider.withings.authorization-uri",
() -> withingsServer.baseUrl() + "/oauth2_user/authorize2");
() -> mockWithingsServer.baseUrl() + "/oauth2_user/authorize2");
registry.add(
"spring.security.oauth2.client.provider.withings.token-uri",
() -> withingsServer.baseUrl() + "/v2/oauth2");
() -> mockWithingsServer.baseUrl() + "/v2/oauth2");

registry.add("spring.security.oauth2.client.registration.withings-client.client-id",
() -> "test-withings-client-id");
registry.add("spring.security.oauth2.client.registration.withings-client.client-secret",
() -> "test-withings-client-secret");
registry.add("withings.api.uri", () -> mockWithingsServer.baseUrl());
}

@AfterEach
void afterEach() {
authorizedClientRepository.deleteAll();
weightRepository.deleteAll();
}

private void authorizeWithingsOAuth2Client() {
TestAuthorizedClient authorizedClient = TestAuthorizedClient.builder()
.clientRegistrationId("withings-client")
.principalName("rob")
.accessTokenType("Bearer")
.accessTokenValue("test-access-token".getBytes(StandardCharsets.UTF_8))
.accessTokenIssuedAt(LocalDateTime.now())
.accessTokenExpiresAt(LocalDateTime.now().plusDays(1))
.accessTokenScopes("user.metrics")
.refreshTokenValue("test-refresh-token".getBytes(StandardCharsets.UTF_8))
.refreshTokenIssuedAt(LocalDateTime.now())
.build();

authorizedClientRepository.save(authorizedClient);
}

@Test
Expand All @@ -86,12 +110,40 @@ public void returns_not_authorized_if_no_preauth_headers_are_sent() throws Excep
assertThat(response.getStatus()).isEqualTo(401);
}

@Test
public void returns_forbidden_if_user_has_no_user_role() throws Exception {
MockHttpServletResponse response = mockMvc
.perform(
get("/weight")
.headers(getAuthHeaders("guest")))
.andReturn().getResponse();

assertThat(response.getStatus()).isEqualTo(403);
}

@Test
public void returns_weight_from_database() throws Exception {
Weight weight = Weight.builder()
.value(83.5)
.createdAt(Instant.now())
.build();
weightRepository.save(weight);

MockHttpServletResponse response = mockMvc
.perform(
get("/weight")
.headers(getAuthHeaders("user")))
.andReturn().getResponse();

assertThat(JsonPath.parse(response.getContentAsString()).read("$.weight", Double.class)).isEqualTo(83.5);
}

@Test
public void returns_not_authorized_if_authorized_client_is_not_found() throws Exception {
MockHttpServletResponse response = mockMvc
.perform(
post("/weight/pull-from-withings")
.headers(getAuthHeaders("guest")))
.headers(getAuthHeaders("user")))
.andReturn().getResponse();

assertThat(response.getStatus()).isEqualTo(401);
Expand All @@ -109,7 +161,7 @@ public void redirects_to_withings_request_authorization_page() throws Exception
assertThat(response.getStatus()).isEqualTo(302);
URI redirectUrl = new URI(response.getRedirectedUrl());
assertThat(redirectUrl).hasHost("localhost");
assertThat(redirectUrl).hasPort(withingsServer.getPort());
assertThat(redirectUrl).hasPort(mockWithingsServer.getPort());
assertThat(redirectUrl).hasPath("/oauth2_user/authorize2");
assertThat(redirectUrl).hasParameter(OAuth2ParameterNames.RESPONSE_TYPE, "code");
assertThat(redirectUrl).hasParameter(OAuth2ParameterNames.CLIENT_ID, "test-withings-client-id");
Expand All @@ -121,7 +173,7 @@ public void redirects_to_withings_request_authorization_page() throws Exception

@Test
public void requests_withings_access_token_after_consent_is_granted() throws Exception {
withingsServer.stubFor(WireMock.post("/v2/oauth2").willReturn(
mockWithingsServer.stubFor(WireMock.post("/v2/oauth2").willReturn(
WireMock.aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBodyFile("withings-authorize.json")));
Expand All @@ -147,20 +199,48 @@ public void requests_withings_access_token_after_consent_is_granted() throws Exc
assertThat(response2.getStatus()).isEqualTo(302);
assertThat(response2.getRedirectedUrl()).isEqualTo("http://localhost/");

List<LoggedRequest> requests = withingsServer.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/v2/oauth2")));
List<LoggedRequest> requests = mockWithingsServer
.findAll(WireMock.postRequestedFor(WireMock.urlEqualTo("/v2/oauth2")));
assertThat(requests).hasSize(1);
URI uri = new URI("?" + requests.get(0).getBodyAsString());

Optional<TestAuthorizedClient> authorizedClient = authorizedClientRepository.findById(WithingsClient.id);

assertThat(authorizedClient.isPresent()).isTrue();
assertThat(authorizedClient.get().getPrincipalName()).isEqualTo("rob");
assertThat(new String(authorizedClient.get().getAccessTokenValue(), "UTF-8")).isEqualTo("test-access-token");
assertThat(new String(authorizedClient.get().getRefreshTokenValue(), "UTF-8")).isEqualTo("test-refresh-token");
assertThat(new String(authorizedClient.get().getAccessTokenValue(), StandardCharsets.UTF_8))
.isEqualTo("test-access-token");
assertThat(new String(authorizedClient.get().getRefreshTokenValue(), StandardCharsets.UTF_8))
.isEqualTo("test-refresh-token");
assertThat(uri).hasParameter(OAuth2ParameterNames.GRANT_TYPE, "authorization_code");
assertThat(uri).hasParameter(OAuth2ParameterNames.CODE, "test-authorization-code");
assertThat(uri).hasParameter("action", "requesttoken");
assertThat(uri).hasParameter(OAuth2ParameterNames.CLIENT_ID, "test-withings-client-id");
assertThat(uri).hasParameter(OAuth2ParameterNames.CLIENT_SECRET, "test-withings-client-secret");
}

@Test
public void pulls_todays_weight_from_withings_to_database() throws Exception {
authorizeWithingsOAuth2Client();
long startTime = LocalDateTime.of(LocalDate.now(), LocalTime.MIN).toInstant(ZoneOffset.UTC).getEpochSecond();
long endTime = LocalDateTime.of(LocalDate.now(), LocalTime.MAX).toInstant(ZoneOffset.UTC).getEpochSecond();
mockWithingsServer.stubFor(WireMock
.post(String.format("/measure?action=getmeas&meastype=1&category=1&startdate=%s&enddate=%s",
startTime, endTime))
.willReturn(
WireMock.aResponse()
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBodyFile("withings-measure.json")));
MockHttpServletResponse response = mockMvc
.perform(
post("/weight/pull-from-withings")
.headers(getAuthHeaders("user")))
.andReturn().getResponse();

assertThat(response.getStatus()).isEqualTo(200);
Optional<Weight> weight = weightRepository.findAll().stream().findFirst();
assertThat(weight.isPresent()).isTrue();
assertThat(weight.get().getValue()).isEqualTo(65.75);
assertThat(weight.get().getCreatedAt().getEpochSecond()).isEqualTo(1594245600L);
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
package mucsi96.traininglog.model;

import org.hibernate.annotations.JdbcType;
import org.hibernate.annotations.Type;
import org.hibernate.type.descriptor.jdbc.VarbinaryJdbcType;
import java.time.LocalDateTime;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder()
@Entity
@Table(name = "oauth2_authorized_client")
@AllArgsConstructor
Expand All @@ -24,9 +23,9 @@ public class TestAuthorizedClient {
private String principalName;
private String accessTokenType;
private byte[] accessTokenValue;
private String accessTokenIssuedAt;
private String accessTokenExpiresAt;
private LocalDateTime accessTokenIssuedAt;
private LocalDateTime accessTokenExpiresAt;
private String accessTokenScopes;
private byte[] refreshTokenValue;
private String refreshTokenIssuedAt;
private LocalDateTime refreshTokenIssuedAt;
}
Loading

0 comments on commit 4801595

Please sign in to comment.