Add Komoot activity import with guided browser-based batch flow #25
4 changed files with 249 additions and 18 deletions
|
|
@ -4,6 +4,8 @@ import jakarta.validation.constraints.Email;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.Pattern;
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request payload for fetching completed activities from Komoot.
|
* Request payload for fetching completed activities from Komoot.
|
||||||
*
|
*
|
||||||
|
|
@ -19,6 +21,19 @@ public record KomootImportRequest(
|
||||||
|
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only")
|
@Pattern(regexp = "\\d+", message = "Komoot user ID must contain digits only")
|
||||||
String userId
|
String userId,
|
||||||
|
|
||||||
|
LocalDate startDate,
|
||||||
|
|
||||||
|
LocalDate endDate
|
||||||
) {
|
) {
|
||||||
|
public KomootImportRequest {
|
||||||
|
boolean onlyOneDateProvided = (startDate == null) != (endDate == null);
|
||||||
|
if (onlyOneDateProvided) {
|
||||||
|
throw new IllegalArgumentException("Start date and end date must either both be set or both be empty.");
|
||||||
|
}
|
||||||
|
if (startDate != null && startDate.isAfter(endDate)) {
|
||||||
|
throw new IllegalArgumentException("Start date must be before or equal to end date.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,12 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
|
@ -47,6 +52,7 @@ public class KomootImportService {
|
||||||
|
|
||||||
private static final int PAGE_SIZE = 100;
|
private static final int PAGE_SIZE = 100;
|
||||||
private static final String KOMOOT_LANGUAGE = "en";
|
private static final String KOMOOT_LANGUAGE = "en";
|
||||||
|
private static final DateTimeFormatter KOMOOT_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX");
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final ActivityRepository activityRepository;
|
private final ActivityRepository activityRepository;
|
||||||
private final ActivityFileService activityFileService;
|
private final ActivityFileService activityFileService;
|
||||||
|
|
@ -58,7 +64,7 @@ public class KomootImportService {
|
||||||
public KomootActivitiesResponse fetchCompletedActivities(KomootImportRequest request) {
|
public KomootActivitiesResponse fetchCompletedActivities(KomootImportRequest request) {
|
||||||
List<KomootActivitySummaryDTO> activities = new ArrayList<>();
|
List<KomootActivitySummaryDTO> activities = new ArrayList<>();
|
||||||
|
|
||||||
URI nextUri = buildInitialUri(request.userId());
|
URI nextUri = buildInitialUri(request);
|
||||||
HttpEntity<Void> httpEntity = new HttpEntity<>(buildHeaders(request.email(), request.password()));
|
HttpEntity<Void> httpEntity = new HttpEntity<>(buildHeaders(request.email(), request.password()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -149,19 +155,25 @@ public class KomootImportService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI buildInitialUri(String userId) {
|
private URI buildInitialUri(KomootImportRequest request) {
|
||||||
String normalizedBaseUrl = komootBaseUrl.endsWith("/") ? komootBaseUrl.substring(0, komootBaseUrl.length() - 1) : komootBaseUrl;
|
String normalizedBaseUrl = komootBaseUrl.endsWith("/") ? komootBaseUrl.substring(0, komootBaseUrl.length() - 1) : komootBaseUrl;
|
||||||
return UriComponentsBuilder.fromUriString(normalizedBaseUrl + "/api/v007/users/" + userId + "/tours/")
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(normalizedBaseUrl + "/api/v007/users/" + request.userId() + "/tours/")
|
||||||
.queryParam("type", "tour_recorded")
|
.queryParam("type", "tour_recorded")
|
||||||
.queryParam("status", "private")
|
|
||||||
.queryParam("name", "")
|
|
||||||
.queryParam("hl", KOMOOT_LANGUAGE)
|
|
||||||
.queryParam("sort_field", "date")
|
.queryParam("sort_field", "date")
|
||||||
.queryParam("sort_direction", "desc")
|
.queryParam("sort_direction", "desc")
|
||||||
.queryParam("page", 0)
|
.queryParam("limit", PAGE_SIZE);
|
||||||
.queryParam("limit", PAGE_SIZE)
|
|
||||||
.build()
|
if (request.startDate() != null && request.endDate() != null) {
|
||||||
.toUri();
|
builder.queryParam("start_date", formatKomootStartDate(request.startDate()))
|
||||||
|
.queryParam("end_date", formatKomootEndDate(request.endDate()));
|
||||||
|
} else {
|
||||||
|
builder.queryParam("status", "private")
|
||||||
|
.queryParam("name", "")
|
||||||
|
.queryParam("hl", KOMOOT_LANGUAGE)
|
||||||
|
.queryParam("page", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build().toUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI buildDetailUri(long activityId) {
|
private URI buildDetailUri(long activityId) {
|
||||||
|
|
@ -314,6 +326,21 @@ public class KomootImportService {
|
||||||
return new ImportCandidateContext(importedKomootActivityIds, activities, candidate);
|
return new ImportCandidateContext(importedKomootActivityIds, activities, candidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatKomootStartDate(LocalDate localDate) {
|
||||||
|
return localDate.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.atOffset(ZoneOffset.UTC)
|
||||||
|
.format(KOMOOT_DATE_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatKomootEndDate(LocalDate localDate) {
|
||||||
|
return localDate.atTime(LocalTime.of(23, 59, 59, 999_000_000))
|
||||||
|
.atZone(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.atOffset(ZoneOffset.UTC)
|
||||||
|
.format(KOMOOT_DATE_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
private Activity.Visibility mapVisibility(String komootStatus) {
|
private Activity.Visibility mapVisibility(String komootStatus) {
|
||||||
if (komootStatus == null) {
|
if (komootStatus == null) {
|
||||||
return Activity.Visibility.PRIVATE;
|
return Activity.Visibility.PRIVATE;
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,19 @@
|
||||||
<input type="text" class="form-control" id="userId" name="userId" required inputmode="numeric" pattern="[0-9]+">
|
<input type="text" class="form-control" id="userId" name="userId" required inputmode="numeric" pattern="[0-9]+">
|
||||||
<div class="form-text">You can find the Komoot ID in your Komoot account settings.</div>
|
<div class="form-text">You can find the Komoot ID in your Komoot account settings.</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="startDate" class="form-label">Start Date</label>
|
||||||
|
<input type="date" class="form-control" id="startDate" name="startDate">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="endDate" class="form-label">End Date</label>
|
||||||
|
<input type="date" class="form-control" id="endDate" name="endDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-text">Both dates must be set together. Inclusive, day-based filter.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 d-flex justify-content-end gap-2 flex-wrap">
|
<div class="mt-4 d-flex justify-content-end gap-2 flex-wrap">
|
||||||
|
|
@ -225,13 +238,21 @@
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
|
const startDate = formData.get('startDate');
|
||||||
|
const endDate = formData.get('endDate');
|
||||||
return {
|
return {
|
||||||
email: formData.get('email'),
|
email: formData.get('email'),
|
||||||
password: formData.get('password'),
|
password: formData.get('password'),
|
||||||
userId: formData.get('userId')
|
userId: formData.get('userId'),
|
||||||
|
startDate: startDate || null,
|
||||||
|
endDate: endDate || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasIncompleteDateRange(payload) {
|
||||||
|
return Boolean(payload.startDate) !== Boolean(payload.endDate);
|
||||||
|
}
|
||||||
|
|
||||||
form.addEventListener('submit', async function(event) {
|
form.addEventListener('submit', async function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
clearError();
|
clearError();
|
||||||
|
|
@ -239,6 +260,10 @@
|
||||||
resultsSection.classList.add('d-none');
|
resultsSection.classList.add('d-none');
|
||||||
|
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
|
if (hasIncompleteDateRange(payload)) {
|
||||||
|
showError('Start date and end date must either both be set or both be empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
|
|
@ -280,6 +305,10 @@
|
||||||
resetAlertToError();
|
resetAlertToError();
|
||||||
|
|
||||||
const payload = buildPayload();
|
const payload = buildPayload();
|
||||||
|
if (hasIncompleteDateRange(payload)) {
|
||||||
|
showError('Start date and end date must either both be set or both be empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setImportLoading(true);
|
setImportLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import net.javahippie.fitpub.model.dto.KomootImportRequest;
|
||||||
import net.javahippie.fitpub.model.entity.Activity;
|
import net.javahippie.fitpub.model.entity.Activity;
|
||||||
import net.javahippie.fitpub.repository.ActivityRepository;
|
import net.javahippie.fitpub.repository.ActivityRepository;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
|
|
@ -16,11 +17,14 @@ import org.springframework.test.web.client.MockRestServiceServer;
|
||||||
import org.springframework.web.client.RestTemplate;
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.TimeZone;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
@ -39,9 +43,12 @@ class KomootImportServiceTest {
|
||||||
private ActivityRepository activityRepository;
|
private ActivityRepository activityRepository;
|
||||||
private ActivityFileService activityFileService;
|
private ActivityFileService activityFileService;
|
||||||
private ActivityPostProcessingService activityPostProcessingService;
|
private ActivityPostProcessingService activityPostProcessingService;
|
||||||
|
private TimeZone originalTimeZone;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
originalTimeZone = TimeZone.getDefault();
|
||||||
|
TimeZone.setDefault(TimeZone.getTimeZone("Europe/Zurich"));
|
||||||
restTemplate = new RestTemplate();
|
restTemplate = new RestTemplate();
|
||||||
server = MockRestServiceServer.bindTo(restTemplate).build();
|
server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
activityRepository = mock(ActivityRepository.class);
|
activityRepository = mock(ActivityRepository.class);
|
||||||
|
|
@ -51,12 +58,17 @@ class KomootImportServiceTest {
|
||||||
ReflectionTestUtils.setField(service, "komootBaseUrl", "https://www.komoot.com");
|
ReflectionTestUtils.setField(service, "komootBaseUrl", "https://www.komoot.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
TimeZone.setDefault(originalTimeZone);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFetchAndMergePagedCompletedActivities() {
|
void shouldFetchAndMergePagedCompletedActivities() {
|
||||||
String authHeader = "Basic " + Base64.getEncoder()
|
String authHeader = "Basic " + Base64.getEncoder()
|
||||||
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&status=private&name=&hl=en&sort_field=date&sort_direction=desc&page=0&limit=100"))
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&status=private&name=&hl=en&page=0"))
|
||||||
.andExpect(method(HttpMethod.GET))
|
.andExpect(method(HttpMethod.GET))
|
||||||
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
.andRespond(withSuccess("""
|
.andRespond(withSuccess("""
|
||||||
|
|
@ -111,7 +123,7 @@ class KomootImportServiceTest {
|
||||||
""", MediaType.APPLICATION_JSON));
|
""", MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
KomootActivitiesResponse response = service.fetchCompletedActivities(
|
KomootActivitiesResponse response = service.fetchCompletedActivities(
|
||||||
new KomootImportRequest("user@example.com", "secret", "123456"));
|
new KomootImportRequest("user@example.com", "secret", "123456", null, null));
|
||||||
|
|
||||||
assertThat(response.totalCount()).isEqualTo(2);
|
assertThat(response.totalCount()).isEqualTo(2);
|
||||||
assertThat(response.activities()).hasSize(2);
|
assertThat(response.activities()).hasSize(2);
|
||||||
|
|
@ -122,6 +134,69 @@ class KomootImportServiceTest {
|
||||||
server.verify();
|
server.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should filter loaded Komoot activities by inclusive date range")
|
||||||
|
void shouldFilterCompletedActivitiesByInclusiveDateRange() {
|
||||||
|
String authHeader = "Basic " + Base64.getEncoder()
|
||||||
|
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&start_date=2026-04-25T22:00:00.000Z&end_date=2026-04-27T21:59:59.999Z"))
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
|
.andRespond(withSuccess("""
|
||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"tours": [
|
||||||
|
{
|
||||||
|
"id": 1002,
|
||||||
|
"name": "Included Start",
|
||||||
|
"sport": "hike",
|
||||||
|
"status": "private",
|
||||||
|
"type": "tour_recorded",
|
||||||
|
"date": "2026-04-26T00:00:00+02:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1003,
|
||||||
|
"name": "Included End",
|
||||||
|
"sport": "run",
|
||||||
|
"status": "private",
|
||||||
|
"type": "tour_recorded",
|
||||||
|
"date": "2026-04-27T23:59:59+02:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {}
|
||||||
|
}
|
||||||
|
""", MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
KomootActivitiesResponse response = service.fetchCompletedActivities(
|
||||||
|
new KomootImportRequest(
|
||||||
|
"user@example.com",
|
||||||
|
"secret",
|
||||||
|
"123456",
|
||||||
|
LocalDate.of(2026, 4, 26),
|
||||||
|
LocalDate.of(2026, 4, 27)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertThat(response.totalCount()).isEqualTo(2);
|
||||||
|
assertThat(response.activities()).extracting("id").containsExactly(1002L, 1003L);
|
||||||
|
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should reject incomplete Komoot date range")
|
||||||
|
void shouldRejectIncompleteDateRange() {
|
||||||
|
assertThatThrownBy(() -> new KomootImportRequest(
|
||||||
|
"user@example.com",
|
||||||
|
"secret",
|
||||||
|
"123456",
|
||||||
|
LocalDate.of(2026, 4, 27),
|
||||||
|
null
|
||||||
|
)).isInstanceOf(IllegalArgumentException.class)
|
||||||
|
.hasMessage("Start date and end date must either both be set or both be empty.");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should import newest not-yet-imported Komoot activity via GPX and override metadata")
|
@DisplayName("Should import newest not-yet-imported Komoot activity via GPX and override metadata")
|
||||||
void shouldImportNewestNotYetImportedActivity() {
|
void shouldImportNewestNotYetImportedActivity() {
|
||||||
|
|
@ -132,7 +207,7 @@ class KomootImportServiceTest {
|
||||||
|
|
||||||
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of());
|
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of());
|
||||||
|
|
||||||
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&status=private&name=&hl=en&sort_field=date&sort_direction=desc&page=0&limit=100"))
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&status=private&name=&hl=en&page=0"))
|
||||||
.andExpect(method(HttpMethod.GET))
|
.andExpect(method(HttpMethod.GET))
|
||||||
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
.andRespond(withSuccess("""
|
.andRespond(withSuccess("""
|
||||||
|
|
@ -190,7 +265,7 @@ class KomootImportServiceTest {
|
||||||
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
KomootImportExecutionResponse response = service.importFirstNewActivity(
|
KomootImportExecutionResponse response = service.importFirstNewActivity(
|
||||||
new KomootImportRequest("user@example.com", "secret", "123456"),
|
new KomootImportRequest("user@example.com", "secret", "123456", null, null),
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -206,6 +281,91 @@ class KomootImportServiceTest {
|
||||||
server.verify();
|
server.verify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Should respect date range when choosing Komoot import candidate")
|
||||||
|
void shouldRespectDateRangeWhenImportingFirstNewActivity() {
|
||||||
|
String authHeader = "Basic " + Base64.getEncoder()
|
||||||
|
.encodeToString("user@example.com:secret".getBytes(StandardCharsets.UTF_8));
|
||||||
|
UUID userId = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
|
||||||
|
UUID importedActivityId = UUID.fromString("dddddddd-dddd-dddd-dddd-dddddddddddd");
|
||||||
|
|
||||||
|
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of());
|
||||||
|
|
||||||
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&start_date=2026-04-26T22:00:00.000Z&end_date=2026-04-27T21:59:59.999Z"))
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
|
.andRespond(withSuccess("""
|
||||||
|
{
|
||||||
|
"_embedded": {
|
||||||
|
"tours": [
|
||||||
|
{
|
||||||
|
"id": 3002,
|
||||||
|
"name": "Inside Range Candidate",
|
||||||
|
"sport": "mtb_easy",
|
||||||
|
"status": "public",
|
||||||
|
"type": "tour_recorded",
|
||||||
|
"date": "2026-04-27T18:15:00+02:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"_links": {}
|
||||||
|
}
|
||||||
|
""", MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/tours/3002?hl=en"))
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
|
.andRespond(withSuccess("""
|
||||||
|
{
|
||||||
|
"id": "3002",
|
||||||
|
"name": "Inside Range Candidate",
|
||||||
|
"description": "Imported from Komoot",
|
||||||
|
"status": "public",
|
||||||
|
"sport": "mtb_easy"
|
||||||
|
}
|
||||||
|
""", MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/tours/3002.gpx"))
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
|
.andRespond(withSuccess("""
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<gpx version="1.1" creator="komoot">
|
||||||
|
<trk><name>Inside Range Candidate</name></trk>
|
||||||
|
</gpx>
|
||||||
|
""", MediaType.APPLICATION_XML));
|
||||||
|
|
||||||
|
Activity importedActivity = Activity.builder()
|
||||||
|
.id(importedActivityId)
|
||||||
|
.userId(userId)
|
||||||
|
.activityType(Activity.ActivityType.OTHER)
|
||||||
|
.title("GPX Title")
|
||||||
|
.description(null)
|
||||||
|
.visibility(Activity.Visibility.PRIVATE)
|
||||||
|
.sourceFileFormat("GPX")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
when(activityFileService.processActivityFile(any(), any(), any(), any(), any())).thenReturn(importedActivity);
|
||||||
|
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
KomootImportExecutionResponse response = service.importFirstNewActivity(
|
||||||
|
new KomootImportRequest(
|
||||||
|
"user@example.com",
|
||||||
|
"secret",
|
||||||
|
"123456",
|
||||||
|
LocalDate.of(2026, 4, 27),
|
||||||
|
LocalDate.of(2026, 4, 27)
|
||||||
|
),
|
||||||
|
userId
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(response.importedKomootActivityId()).isEqualTo(3002L);
|
||||||
|
assertThat(importedActivity.getKomootActivityId()).isEqualTo(3002L);
|
||||||
|
|
||||||
|
verify(activityPostProcessingService).processActivityAsync(importedActivityId, userId);
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("Should fall back to OTHER when Komoot sport cannot be mapped")
|
@DisplayName("Should fall back to OTHER when Komoot sport cannot be mapped")
|
||||||
void shouldFallbackToOtherForUnknownKomootSport() {
|
void shouldFallbackToOtherForUnknownKomootSport() {
|
||||||
|
|
@ -216,7 +376,7 @@ class KomootImportServiceTest {
|
||||||
|
|
||||||
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of());
|
when(activityRepository.findImportedKomootActivityIdsByUserId(userId)).thenReturn(List.of());
|
||||||
|
|
||||||
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&status=private&name=&hl=en&sort_field=date&sort_direction=desc&page=0&limit=100"))
|
server.expect(once(), requestTo("https://www.komoot.com/api/v007/users/123456/tours/?type=tour_recorded&sort_field=date&sort_direction=desc&limit=100&status=private&name=&hl=en&page=0"))
|
||||||
.andExpect(method(HttpMethod.GET))
|
.andExpect(method(HttpMethod.GET))
|
||||||
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
.andExpect(header(HttpHeaders.AUTHORIZATION, authHeader))
|
||||||
.andRespond(withSuccess("""
|
.andRespond(withSuccess("""
|
||||||
|
|
@ -274,7 +434,7 @@ class KomootImportServiceTest {
|
||||||
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
KomootImportExecutionResponse response = service.importFirstNewActivity(
|
KomootImportExecutionResponse response = service.importFirstNewActivity(
|
||||||
new KomootImportRequest("user@example.com", "secret", "123456"),
|
new KomootImportRequest("user@example.com", "secret", "123456", null, null),
|
||||||
userId
|
userId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue