Fit File Processing and Persistence
This commit is contained in:
commit
0bc4fb3118
24 changed files with 3533 additions and 0 deletions
|
|
@ -0,0 +1,370 @@
|
|||
package org.operaton.fitpub.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.operaton.fitpub.exception.FitFileProcessingException;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
import org.operaton.fitpub.repository.ActivityMetricsRepository;
|
||||
import org.operaton.fitpub.repository.ActivityRepository;
|
||||
import org.operaton.fitpub.util.FitFileValidator;
|
||||
import org.operaton.fitpub.util.FitParser;
|
||||
import org.operaton.fitpub.util.TrackSimplifier;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* Unit tests for FitFileService.
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FitFileServiceTest {
|
||||
|
||||
@Mock
|
||||
private FitFileValidator validator;
|
||||
|
||||
@Mock
|
||||
private FitParser parser;
|
||||
|
||||
@Mock
|
||||
private TrackSimplifier trackSimplifier;
|
||||
|
||||
@Mock
|
||||
private ActivityRepository activityRepository;
|
||||
|
||||
@Mock
|
||||
private ActivityMetricsRepository metricsRepository;
|
||||
|
||||
@Spy
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@InjectMocks
|
||||
private FitFileService fitFileService;
|
||||
|
||||
private UUID testUserId;
|
||||
private MockMultipartFile testFile;
|
||||
private FitParser.ParsedFitData testParsedData;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testUserId = UUID.randomUUID();
|
||||
testFile = new MockMultipartFile(
|
||||
"file",
|
||||
"test-activity.fit",
|
||||
"application/octet-stream",
|
||||
new byte[100]
|
||||
);
|
||||
|
||||
// Configure ObjectMapper for Java 8 Time
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
|
||||
// Create test parsed data
|
||||
testParsedData = createTestParsedData();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully process a valid FIT file")
|
||||
void testProcessFitFileSuccess() throws Exception {
|
||||
// Arrange
|
||||
when(parser.parse(any(byte[].class))).thenReturn(testParsedData);
|
||||
when(trackSimplifier.simplify(any())).thenAnswer(invocation -> {
|
||||
Coordinate[] coords = invocation.getArgument(0);
|
||||
return new GeometryFactory().createLineString(coords);
|
||||
});
|
||||
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> {
|
||||
Activity activity = invocation.getArgument(0);
|
||||
activity.setId(UUID.randomUUID());
|
||||
activity.setCreatedAt(LocalDateTime.now());
|
||||
activity.setUpdatedAt(LocalDateTime.now());
|
||||
return activity;
|
||||
});
|
||||
|
||||
// Act
|
||||
Activity result = fitFileService.processFitFile(
|
||||
testFile,
|
||||
testUserId,
|
||||
"Test Run",
|
||||
"Morning run",
|
||||
Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals("Test Run", result.getTitle());
|
||||
assertEquals("Morning run", result.getDescription());
|
||||
assertEquals(testUserId, result.getUserId());
|
||||
assertEquals(Activity.Visibility.PUBLIC, result.getVisibility());
|
||||
assertEquals(Activity.ActivityType.RUN, result.getActivityType());
|
||||
|
||||
verify(validator).validate(any(), anyLong());
|
||||
verify(parser).parse(any(byte[].class));
|
||||
verify(activityRepository).save(any(Activity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should generate default title when title is null")
|
||||
void testProcessFitFileWithDefaultTitle() throws Exception {
|
||||
// Arrange
|
||||
when(parser.parse(any(byte[].class))).thenReturn(testParsedData);
|
||||
when(trackSimplifier.simplify(any())).thenAnswer(invocation -> {
|
||||
Coordinate[] coords = invocation.getArgument(0);
|
||||
return new GeometryFactory().createLineString(coords);
|
||||
});
|
||||
when(activityRepository.save(any(Activity.class))).thenAnswer(invocation -> {
|
||||
Activity activity = invocation.getArgument(0);
|
||||
activity.setId(UUID.randomUUID());
|
||||
return activity;
|
||||
});
|
||||
|
||||
// Act
|
||||
Activity result = fitFileService.processFitFile(
|
||||
testFile,
|
||||
testUserId,
|
||||
null,
|
||||
null,
|
||||
Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertTrue(result.getTitle().contains("Run"));
|
||||
assertTrue(result.getTitle().contains(testParsedData.getStartTime().toLocalDate().toString()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when validator fails")
|
||||
void testProcessFitFileValidationFailure() throws Exception {
|
||||
// Arrange
|
||||
doThrow(new FitFileProcessingException("Invalid file"))
|
||||
.when(validator).validate(any(), anyLong());
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(FitFileProcessingException.class, () ->
|
||||
fitFileService.processFitFile(
|
||||
testFile,
|
||||
testUserId,
|
||||
"Test",
|
||||
null,
|
||||
Activity.Visibility.PUBLIC
|
||||
)
|
||||
);
|
||||
|
||||
verify(parser, never()).parse(any(byte[].class));
|
||||
verify(activityRepository, never()).save(any(Activity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception when parser fails")
|
||||
void testProcessFitFileParsingFailure() throws Exception {
|
||||
// Arrange
|
||||
when(parser.parse(any(byte[].class)))
|
||||
.thenThrow(new FitFileProcessingException("Parsing failed"));
|
||||
|
||||
// Act & Assert
|
||||
assertThrows(FitFileProcessingException.class, () ->
|
||||
fitFileService.processFitFile(
|
||||
testFile,
|
||||
testUserId,
|
||||
"Test",
|
||||
null,
|
||||
Activity.Visibility.PUBLIC
|
||||
)
|
||||
);
|
||||
|
||||
verify(activityRepository, never()).save(any(Activity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully delete an activity")
|
||||
void testDeleteActivity() {
|
||||
// Arrange
|
||||
UUID activityId = UUID.randomUUID();
|
||||
Activity activity = Activity.builder()
|
||||
.id(activityId)
|
||||
.userId(testUserId)
|
||||
.build();
|
||||
|
||||
when(activityRepository.findByIdAndUserId(activityId, testUserId))
|
||||
.thenReturn(Optional.of(activity));
|
||||
|
||||
// Act
|
||||
boolean result = fitFileService.deleteActivity(activityId, testUserId);
|
||||
|
||||
// Assert
|
||||
assertTrue(result);
|
||||
verify(activityRepository).delete(activity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return false when deleting non-existent activity")
|
||||
void testDeleteNonExistentActivity() {
|
||||
// Arrange
|
||||
UUID activityId = UUID.randomUUID();
|
||||
when(activityRepository.findByIdAndUserId(activityId, testUserId))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
boolean result = fitFileService.deleteActivity(activityId, testUserId);
|
||||
|
||||
// Assert
|
||||
assertFalse(result);
|
||||
verify(activityRepository, never()).delete(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should retrieve activity by ID and user ID")
|
||||
void testGetActivity() {
|
||||
// Arrange
|
||||
UUID activityId = UUID.randomUUID();
|
||||
Activity activity = Activity.builder()
|
||||
.id(activityId)
|
||||
.userId(testUserId)
|
||||
.title("Test Activity")
|
||||
.build();
|
||||
|
||||
when(activityRepository.findByIdAndUserId(activityId, testUserId))
|
||||
.thenReturn(Optional.of(activity));
|
||||
|
||||
// Act
|
||||
Activity result = fitFileService.getActivity(activityId, testUserId);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(activityId, result.getId());
|
||||
assertEquals("Test Activity", result.getTitle());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return null for non-existent activity")
|
||||
void testGetNonExistentActivity() {
|
||||
// Arrange
|
||||
UUID activityId = UUID.randomUUID();
|
||||
when(activityRepository.findByIdAndUserId(activityId, testUserId))
|
||||
.thenReturn(Optional.empty());
|
||||
|
||||
// Act
|
||||
Activity result = fitFileService.getActivity(activityId, testUserId);
|
||||
|
||||
// Assert
|
||||
assertNull(result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should retrieve all activities for a user")
|
||||
void testGetUserActivities() {
|
||||
// Arrange
|
||||
List<Activity> activities = new ArrayList<>();
|
||||
activities.add(Activity.builder().id(UUID.randomUUID()).userId(testUserId).build());
|
||||
activities.add(Activity.builder().id(UUID.randomUUID()).userId(testUserId).build());
|
||||
|
||||
when(activityRepository.findByUserIdOrderByStartedAtDesc(testUserId))
|
||||
.thenReturn(activities);
|
||||
|
||||
// Act
|
||||
List<Activity> result = fitFileService.getUserActivities(testUserId);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should process FIT file with metrics")
|
||||
void testProcessFitFileWithMetrics() throws Exception {
|
||||
// Arrange
|
||||
when(parser.parse(any(byte[].class))).thenReturn(testParsedData);
|
||||
when(trackSimplifier.simplify(any())).thenAnswer(invocation -> {
|
||||
Coordinate[] coords = invocation.getArgument(0);
|
||||
return new GeometryFactory().createLineString(coords);
|
||||
});
|
||||
|
||||
ArgumentCaptor<Activity> activityCaptor = ArgumentCaptor.forClass(Activity.class);
|
||||
when(activityRepository.save(activityCaptor.capture())).thenAnswer(invocation -> {
|
||||
Activity activity = invocation.getArgument(0);
|
||||
activity.setId(UUID.randomUUID());
|
||||
return activity;
|
||||
});
|
||||
|
||||
// Act
|
||||
Activity result = fitFileService.processFitFile(
|
||||
testFile,
|
||||
testUserId,
|
||||
"Complete Activity",
|
||||
null,
|
||||
Activity.Visibility.PUBLIC
|
||||
);
|
||||
|
||||
// Assert
|
||||
assertNotNull(result);
|
||||
Activity savedActivity = activityCaptor.getValue();
|
||||
|
||||
assertNotNull(savedActivity.getSimplifiedTrack());
|
||||
assertNotNull(savedActivity.getTrackPointsJson());
|
||||
assertNotNull(savedActivity.getMetrics());
|
||||
assertEquals(testParsedData.getTotalDistance(), savedActivity.getTotalDistance());
|
||||
assertEquals(testParsedData.getTotalDuration(), savedActivity.getTotalDuration());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test parsed FIT data with realistic values.
|
||||
*/
|
||||
private FitParser.ParsedFitData createTestParsedData() {
|
||||
FitParser.ParsedFitData data = new FitParser.ParsedFitData();
|
||||
|
||||
LocalDateTime startTime = LocalDateTime.of(2024, 1, 15, 8, 0, 0);
|
||||
data.setStartTime(startTime);
|
||||
data.setEndTime(startTime.plusMinutes(30));
|
||||
data.setActivityType(Activity.ActivityType.RUN);
|
||||
data.setTotalDistance(BigDecimal.valueOf(5000.0));
|
||||
data.setTotalDuration(Duration.ofMinutes(30));
|
||||
data.setElevationGain(BigDecimal.valueOf(100.0));
|
||||
data.setElevationLoss(BigDecimal.valueOf(95.0));
|
||||
|
||||
// Add test track points
|
||||
List<FitParser.TrackPointData> trackPoints = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
FitParser.TrackPointData tp = new FitParser.TrackPointData();
|
||||
tp.setTimestamp(startTime.plusMinutes(i * 3));
|
||||
tp.setLatitude(47.0 + i * 0.001);
|
||||
tp.setLongitude(8.0 + i * 0.001);
|
||||
tp.setElevation(BigDecimal.valueOf(500 + i * 10));
|
||||
tp.setHeartRate(140 + i);
|
||||
tp.setSpeed(BigDecimal.valueOf(10.0));
|
||||
trackPoints.add(tp);
|
||||
}
|
||||
|
||||
data.setTrackPoints(trackPoints);
|
||||
|
||||
// Add test metrics
|
||||
FitParser.ActivityMetricsData metrics = new FitParser.ActivityMetricsData();
|
||||
metrics.setAverageSpeed(BigDecimal.valueOf(10.0));
|
||||
metrics.setMaxSpeed(BigDecimal.valueOf(15.0));
|
||||
metrics.setAverageHeartRate(150);
|
||||
metrics.setMaxHeartRate(170);
|
||||
metrics.setCalories(300);
|
||||
data.setMetrics(metrics);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
152
src/test/java/org/operaton/fitpub/util/FitFileValidatorTest.java
Normal file
152
src/test/java/org/operaton/fitpub/util/FitFileValidatorTest.java
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
package org.operaton.fitpub.util;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.operaton.fitpub.exception.InvalidFitFileException;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for FitFileValidator.
|
||||
*/
|
||||
class FitFileValidatorTest {
|
||||
|
||||
private FitFileValidator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validator = new FitFileValidator();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate a valid FIT file header")
|
||||
void testValidateValidHeader() {
|
||||
byte[] validHeader = TestFitFileGenerator.generateValidFitFileHeader();
|
||||
|
||||
assertDoesNotThrow(() -> validator.validate(validHeader));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for empty file")
|
||||
void testValidateEmptyFile() {
|
||||
byte[] emptyFile = TestFitFileGenerator.generateEmptyFile();
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(emptyFile)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for null file")
|
||||
void testValidateNullFile() {
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate((byte[]) null)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("empty"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for file that's too small")
|
||||
void testValidateTooSmallFile() {
|
||||
byte[] tooSmall = TestFitFileGenerator.generateTooSmallFile();
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(tooSmall)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("too small"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for file that's too large")
|
||||
void testValidateTooLargeFile() throws IOException {
|
||||
long tooLarge = 60L * 1024 * 1024; // 60 MB
|
||||
byte[] validHeader = TestFitFileGenerator.generateValidFitFileHeader();
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(validHeader);
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(inputStream, tooLarge)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("too large"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for invalid header size")
|
||||
void testValidateInvalidHeaderSize() {
|
||||
byte[] invalidHeader = TestFitFileGenerator.generateInvalidHeaderSize();
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(invalidHeader)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("Invalid FIT header size"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for invalid signature")
|
||||
void testValidateInvalidSignature() {
|
||||
byte[] invalidSignature = TestFitFileGenerator.generateInvalidSignature();
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(invalidSignature)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("Invalid FIT file signature"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate file from input stream")
|
||||
void testValidateFromInputStream() throws IOException {
|
||||
byte[] validHeader = TestFitFileGenerator.generateValidFitFileHeader();
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(validHeader);
|
||||
|
||||
assertDoesNotThrow(() -> validator.validate(inputStream, validHeader.length));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw exception for input stream with insufficient data")
|
||||
void testValidateInsufficientDataFromStream() throws IOException {
|
||||
byte[] tooSmall = new byte[10];
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(tooSmall);
|
||||
|
||||
InvalidFitFileException exception = assertThrows(
|
||||
InvalidFitFileException.class,
|
||||
() -> validator.validate(inputStream, tooSmall.length)
|
||||
);
|
||||
|
||||
assertTrue(exception.getMessage().contains("too small"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate .fit file extension")
|
||||
void testHasValidExtension() {
|
||||
assertTrue(validator.hasValidExtension("activity.fit"));
|
||||
assertTrue(validator.hasValidExtension("ACTIVITY.FIT"));
|
||||
assertTrue(validator.hasValidExtension("path/to/file.fit"));
|
||||
assertTrue(validator.hasValidExtension("file.FIT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should reject invalid file extensions")
|
||||
void testHasInvalidExtension() {
|
||||
assertFalse(validator.hasValidExtension("activity.gpx"));
|
||||
assertFalse(validator.hasValidExtension("activity.txt"));
|
||||
assertFalse(validator.hasValidExtension("activity"));
|
||||
assertFalse(validator.hasValidExtension(null));
|
||||
assertFalse(validator.hasValidExtension(""));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
package org.operaton.fitpub.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.operaton.fitpub.model.entity.Activity;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Integration test for FitParser using a real FIT file.
|
||||
*/
|
||||
@Slf4j
|
||||
class FitParserIntegrationTest {
|
||||
|
||||
private FitParser parser;
|
||||
private FitFileValidator validator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
parser = new FitParser();
|
||||
validator = new FitFileValidator();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should successfully parse real FIT file from test resources")
|
||||
void testParseRealFitFile() throws IOException {
|
||||
// Load the real FIT file from test resources
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
InputStream inputStream = getClass().getResourceAsStream(fitFileName);
|
||||
|
||||
assertNotNull(inputStream, "FIT file should exist in test resources: " + fitFileName);
|
||||
|
||||
// Read file into byte array
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// Validate the file
|
||||
assertDoesNotThrow(() -> validator.validate(fileData),
|
||||
"Real FIT file should pass validation");
|
||||
|
||||
// Parse the file
|
||||
FitParser.ParsedFitData parsedData = assertDoesNotThrow(
|
||||
() -> parser.parse(fileData),
|
||||
"Real FIT file should parse without errors"
|
||||
);
|
||||
|
||||
// Verify parsed data structure
|
||||
assertNotNull(parsedData, "Parsed data should not be null");
|
||||
|
||||
// Verify track points
|
||||
assertNotNull(parsedData.getTrackPoints(), "Track points should not be null");
|
||||
assertFalse(parsedData.getTrackPoints().isEmpty(), "Track points should not be empty");
|
||||
|
||||
log.info("Successfully parsed real FIT file:");
|
||||
log.info(" Track points: {}", parsedData.getTrackPoints().size());
|
||||
log.info(" Activity type: {}", parsedData.getActivityType());
|
||||
|
||||
if (parsedData.getStartTime() != null) {
|
||||
log.info(" Start time: {}", parsedData.getStartTime());
|
||||
}
|
||||
|
||||
if (parsedData.getEndTime() != null) {
|
||||
log.info(" End time: {}", parsedData.getEndTime());
|
||||
}
|
||||
|
||||
if (parsedData.getTotalDistance() != null) {
|
||||
log.info(" Total distance: {} meters", parsedData.getTotalDistance());
|
||||
}
|
||||
|
||||
if (parsedData.getTotalDuration() != null) {
|
||||
long minutes = parsedData.getTotalDuration().toMinutes();
|
||||
long seconds = parsedData.getTotalDuration().getSeconds() % 60;
|
||||
log.info(" Total duration: {}m {}s", minutes, seconds);
|
||||
}
|
||||
|
||||
if (parsedData.getElevationGain() != null) {
|
||||
log.info(" Elevation gain: {} meters", parsedData.getElevationGain());
|
||||
}
|
||||
|
||||
if (parsedData.getElevationLoss() != null) {
|
||||
log.info(" Elevation loss: {} meters", parsedData.getElevationLoss());
|
||||
}
|
||||
|
||||
// Verify at least some basic data
|
||||
assertNotNull(parsedData.getActivityType(), "Activity type should be determined");
|
||||
assertTrue(parsedData.getTrackPoints().size() > 0, "Should have at least one track point");
|
||||
|
||||
// Verify track point data quality
|
||||
FitParser.TrackPointData firstPoint = parsedData.getTrackPoints().get(0);
|
||||
assertNotNull(firstPoint, "First track point should not be null");
|
||||
assertNotEquals(0.0, firstPoint.getLatitude(), "Latitude should be set");
|
||||
assertNotEquals(0.0, firstPoint.getLongitude(), "Longitude should be set");
|
||||
|
||||
// Verify GPS coordinates are in valid range
|
||||
assertTrue(firstPoint.getLatitude() >= -90 && firstPoint.getLatitude() <= 90,
|
||||
"Latitude should be in valid range (-90 to 90)");
|
||||
assertTrue(firstPoint.getLongitude() >= -180 && firstPoint.getLongitude() <= 180,
|
||||
"Longitude should be in valid range (-180 to 180)");
|
||||
|
||||
log.info(" First point: lat={}, lon={}", firstPoint.getLatitude(), firstPoint.getLongitude());
|
||||
|
||||
if (firstPoint.getElevation() != null) {
|
||||
log.info(" First point elevation: {} meters", firstPoint.getElevation());
|
||||
}
|
||||
|
||||
if (firstPoint.getHeartRate() != null) {
|
||||
log.info(" First point heart rate: {} bpm", firstPoint.getHeartRate());
|
||||
}
|
||||
|
||||
// Verify metrics if present
|
||||
if (parsedData.getMetrics() != null) {
|
||||
FitParser.ActivityMetricsData metrics = parsedData.getMetrics();
|
||||
log.info("Metrics:");
|
||||
|
||||
if (metrics.getAverageSpeed() != null) {
|
||||
log.info(" Average speed: {} km/h", metrics.getAverageSpeed());
|
||||
}
|
||||
|
||||
if (metrics.getMaxSpeed() != null) {
|
||||
log.info(" Max speed: {} km/h", metrics.getMaxSpeed());
|
||||
}
|
||||
|
||||
if (metrics.getAverageHeartRate() != null) {
|
||||
log.info(" Average heart rate: {} bpm", metrics.getAverageHeartRate());
|
||||
}
|
||||
|
||||
if (metrics.getMaxHeartRate() != null) {
|
||||
log.info(" Max heart rate: {} bpm", metrics.getMaxHeartRate());
|
||||
}
|
||||
|
||||
if (metrics.getCalories() != null) {
|
||||
log.info(" Calories: {}", metrics.getCalories());
|
||||
}
|
||||
|
||||
if (metrics.getAverageCadence() != null) {
|
||||
log.info(" Average cadence: {}", metrics.getAverageCadence());
|
||||
}
|
||||
|
||||
if (metrics.getAveragePower() != null) {
|
||||
log.info(" Average power: {} watts", metrics.getAveragePower());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should extract complete activity data from real FIT file")
|
||||
void testExtractCompleteActivityData() throws IOException {
|
||||
// Load the real FIT file
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
InputStream inputStream = getClass().getResourceAsStream(fitFileName);
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// Parse the file
|
||||
FitParser.ParsedFitData parsedData = parser.parse(fileData);
|
||||
|
||||
// Test converting to entity structures
|
||||
Activity.ActivityType activityType = parsedData.getActivityType();
|
||||
assertNotNull(activityType, "Activity type should be extracted");
|
||||
|
||||
// Verify we can convert track points to entities
|
||||
if (!parsedData.getTrackPoints().isEmpty()) {
|
||||
FitParser.TrackPointData trackPointData = parsedData.getTrackPoints().get(0);
|
||||
|
||||
// Test geometry creation
|
||||
assertDoesNotThrow(() -> trackPointData.toGeometry(),
|
||||
"Should be able to create Point geometry from track point");
|
||||
|
||||
var point = trackPointData.toGeometry();
|
||||
assertNotNull(point, "Point geometry should not be null");
|
||||
assertEquals(trackPointData.getLongitude(), point.getX(), 0.0001,
|
||||
"Point X coordinate should match longitude");
|
||||
assertEquals(trackPointData.getLatitude(), point.getY(), 0.0001,
|
||||
"Point Y coordinate should match latitude");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should validate real FIT file successfully")
|
||||
void testValidateRealFitFile() throws IOException {
|
||||
// Load the real FIT file
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
InputStream inputStream = getClass().getResourceAsStream(fitFileName);
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// Should pass all validation checks
|
||||
assertDoesNotThrow(() -> validator.validate(fileData),
|
||||
"Real FIT file should pass validation");
|
||||
|
||||
// File should have valid extension
|
||||
assertTrue(validator.hasValidExtension(fitFileName),
|
||||
"File should have valid .fit extension");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should handle track points in chronological order")
|
||||
void testTrackPointsChronologicalOrder() throws IOException {
|
||||
// Load the real FIT file
|
||||
String fitFileName = "/69287079d5e0a4532ba818ee.fit";
|
||||
InputStream inputStream = getClass().getResourceAsStream(fitFileName);
|
||||
byte[] fileData = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
FitParser.ParsedFitData parsedData = parser.parse(fileData);
|
||||
|
||||
// Verify track points are in chronological order
|
||||
if (parsedData.getTrackPoints().size() > 1) {
|
||||
for (int i = 0; i < parsedData.getTrackPoints().size() - 1; i++) {
|
||||
FitParser.TrackPointData current = parsedData.getTrackPoints().get(i);
|
||||
FitParser.TrackPointData next = parsedData.getTrackPoints().get(i + 1);
|
||||
|
||||
if (current.getTimestamp() != null && next.getTimestamp() != null) {
|
||||
assertTrue(
|
||||
!current.getTimestamp().isAfter(next.getTimestamp()),
|
||||
"Track points should be in chronological order at index " + i
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Track points are in chronological order");
|
||||
log.info(" First timestamp: {}", parsedData.getTrackPoints().get(0).getTimestamp());
|
||||
log.info(" Last timestamp: {}",
|
||||
parsedData.getTrackPoints().get(parsedData.getTrackPoints().size() - 1).getTimestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/test/java/org/operaton/fitpub/util/TestFitFileGenerator.java
Normal file
102
src/test/java/org/operaton/fitpub/util/TestFitFileGenerator.java
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package org.operaton.fitpub.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
/**
|
||||
* Utility class for generating test FIT files.
|
||||
* Creates minimal valid FIT file structures for testing.
|
||||
*/
|
||||
public class TestFitFileGenerator {
|
||||
|
||||
/**
|
||||
* Generates a minimal valid FIT file header.
|
||||
*/
|
||||
public static byte[] generateValidFitFileHeader() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(14);
|
||||
buffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buffer.put((byte) 14); // Header size
|
||||
buffer.put((byte) 0x10); // Protocol version 1.0
|
||||
buffer.putShort((short) 2048); // Profile version
|
||||
buffer.putInt(100); // Data size
|
||||
buffer.put(".FIT".getBytes()); // Signature
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a FIT file with invalid header size.
|
||||
*/
|
||||
public static byte[] generateInvalidHeaderSize() {
|
||||
byte[] header = generateValidFitFileHeader();
|
||||
header[0] = 20; // Invalid header size
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a FIT file with invalid signature.
|
||||
*/
|
||||
public static byte[] generateInvalidSignature() {
|
||||
byte[] header = generateValidFitFileHeader();
|
||||
header[8] = 'X'; // Invalid signature
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a minimal valid FIT file with a single data record.
|
||||
* This creates a very basic but valid FIT file structure.
|
||||
*/
|
||||
public static byte[] generateMinimalValidFitFile() throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
// Write header
|
||||
ByteBuffer header = ByteBuffer.allocate(14);
|
||||
header.order(ByteOrder.LITTLE_ENDIAN);
|
||||
header.put((byte) 14); // Header size
|
||||
header.put((byte) 0x20); // Protocol version 2.0
|
||||
header.putShort((short) 2113); // Profile version 21.13
|
||||
header.putInt(0); // Data size (will update later)
|
||||
header.put(".FIT".getBytes()); // Signature
|
||||
header.putShort((short) 0); // CRC (optional, set to 0)
|
||||
baos.write(header.array());
|
||||
|
||||
// For a real FIT file, we would write definition messages and data messages here
|
||||
// For testing purposes, this minimal header-only file should suffice for validation tests
|
||||
// More complex tests would require actual FIT SDK to generate proper files
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
|
||||
// Update data size in header
|
||||
ByteBuffer dataSize = ByteBuffer.allocate(4);
|
||||
dataSize.order(ByteOrder.LITTLE_ENDIAN);
|
||||
dataSize.putInt(result.length - 14 - 2); // Exclude header and CRC
|
||||
System.arraycopy(dataSize.array(), 0, result, 4, 4);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an empty byte array (invalid FIT file).
|
||||
*/
|
||||
public static byte[] generateEmptyFile() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file that's too small.
|
||||
*/
|
||||
public static byte[] generateTooSmallFile() {
|
||||
return new byte[10];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a very large file (simulated, not actually allocating the memory).
|
||||
*/
|
||||
public static byte[] generateTooLargeFileHeader() {
|
||||
// Just return a header, tests will check the size parameter
|
||||
return generateValidFitFileHeader();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue