Fit File Processing and Persistence

This commit is contained in:
Tim Zöller 2025-11-27 22:59:45 +01:00
commit 0bc4fb3118
24 changed files with 3533 additions and 0 deletions

View file

@ -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;
}
}

View 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(""));
}
}

View file

@ -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());
}
}
}

View 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();
}
}