fix(watch): finalise correctif sync workoutHistory/exercise et distance live montre (#157)
This commit is contained in:
@ -10,6 +10,7 @@ import 'package:gametime/infrastructure/local/local.dart' as local;
|
||||
|
||||
void main() {
|
||||
late local.AppDatabase database;
|
||||
late local.DriftMediaAssetRepository mediaAssetRepository;
|
||||
late local.DriftExerciseRepository exerciseRepository;
|
||||
late local.DriftProgramRepository programRepository;
|
||||
late local.DriftActiveSessionRepository activeRepository;
|
||||
@ -24,6 +25,7 @@ void main() {
|
||||
|
||||
setUp(() {
|
||||
database = local.AppDatabase(NativeDatabase.memory());
|
||||
mediaAssetRepository = local.DriftMediaAssetRepository(database);
|
||||
exerciseRepository = local.DriftExerciseRepository(database);
|
||||
programRepository = local.DriftProgramRepository(database);
|
||||
activeRepository = local.DriftActiveSessionRepository(database);
|
||||
@ -482,6 +484,209 @@ CREATE TABLE pending_share_actions (
|
||||
expect(payloadsById['template-sync-tags']!['tags'], ['routine']);
|
||||
});
|
||||
|
||||
test('local sync payload includes full workout history aggregate', () async {
|
||||
final now = DateTime.utc(2026, 7, 22, 10, 45);
|
||||
await historyRepository.save(
|
||||
_history(
|
||||
id: 'sync-history-full',
|
||||
startedAt: now,
|
||||
result: _historySetResult(
|
||||
id: 'sync-history-set-result',
|
||||
historyId: 'sync-history-full',
|
||||
sourceExerciseId: 'exercise-sync-full',
|
||||
setIndex: 0,
|
||||
startedAt: now,
|
||||
actualScore: 12,
|
||||
),
|
||||
stepResults: [
|
||||
_historyStepResult(
|
||||
id: 'sync-history-step-result',
|
||||
historyId: 'sync-history-full',
|
||||
sourceExerciseId: 'exercise-sync-full',
|
||||
startedAt: now,
|
||||
),
|
||||
],
|
||||
minHeartRateBpm: 90,
|
||||
averageHeartRateBpm: 120,
|
||||
maxHeartRateBpm: 150,
|
||||
totalDistanceMeters: 42,
|
||||
totalCaloriesKcal: 12,
|
||||
),
|
||||
);
|
||||
|
||||
final changes = await syncChangeRepository.listPendingChanges();
|
||||
final payload = changes
|
||||
.singleWhere((change) => change.item.clientId == 'sync-history-full')
|
||||
.item
|
||||
.payload;
|
||||
|
||||
expect(payload['minHeartRateBpm'], 90);
|
||||
expect(payload['averageHeartRateBpm'], 120);
|
||||
expect(payload['maxHeartRateBpm'], 150);
|
||||
expect(payload['totalDistanceMeters'], 42);
|
||||
expect(payload['totalCaloriesKcal'], 12);
|
||||
expect(payload['results'], hasLength(1));
|
||||
expect(payload['stepResults'], hasLength(1));
|
||||
});
|
||||
|
||||
test('local sync pull restores exercise images and steps', () async {
|
||||
final now = DateTime.utc(2026, 7, 22, 10, 50);
|
||||
await mediaAssetRepository.save(
|
||||
MediaAsset(
|
||||
metadata: _metadata('remote-image', now),
|
||||
kind: MediaKind.image,
|
||||
localUri: 'file:///remote-image.png',
|
||||
),
|
||||
);
|
||||
|
||||
final applied = await syncChangeRepository.applyRemoteItem(
|
||||
RemoteSyncedItem(
|
||||
resourceType: SyncResourceType.exercise,
|
||||
clientId: 'remote-exercise-with-children',
|
||||
serverId: 'server-exercise-with-children',
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: now,
|
||||
serverUpdatedAt: now,
|
||||
deletedAt: null,
|
||||
payload: {
|
||||
'id': 'remote-exercise-with-children',
|
||||
'name': 'Remote exercise',
|
||||
'imageMediaIds': const ['remote-image'],
|
||||
'iconMediaId': 'remote-image',
|
||||
'hasTimeMeasure': false,
|
||||
'hasRepsMeasure': true,
|
||||
'hasScoreMeasure': true,
|
||||
'scoreInputMode': 'manual',
|
||||
'scoreLabel': 'Paniers',
|
||||
'scoreUnit': 'pts',
|
||||
'steps': [
|
||||
_exerciseStep(
|
||||
id: 'remote-step',
|
||||
position: 0,
|
||||
name: 'Tir main droite',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 10,
|
||||
hasScore: true,
|
||||
scoreLabel: 'Paniers',
|
||||
scoreUnit: 'pts',
|
||||
linkedToSeriesScore: true,
|
||||
).toSnapshotJson(),
|
||||
],
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final exercise = await exerciseRepository.findById(
|
||||
'remote-exercise-with-children',
|
||||
);
|
||||
|
||||
expect(applied, isTrue);
|
||||
expect(exercise!.imageMediaIds, ['remote-image']);
|
||||
expect(exercise.steps, hasLength(1));
|
||||
expect(exercise.steps.single.linkedToSeriesScore, isTrue);
|
||||
});
|
||||
|
||||
test('local sync pull restores full workout history aggregate', () async {
|
||||
final now = DateTime.utc(2026, 7, 22, 10, 55);
|
||||
final payload = <String, Object?>{
|
||||
'metadata': {
|
||||
'id': 'remote-history-full',
|
||||
'createdAt': now.toUtc().toIso8601String(),
|
||||
'updatedAt': now.toUtc().toIso8601String(),
|
||||
'schemaVersion': 1,
|
||||
'syncState': 'synced',
|
||||
'localRevision': 0,
|
||||
'originDeviceId': 'device-remote',
|
||||
},
|
||||
'id': 'remote-history-full',
|
||||
'nameSnapshot': 'Remote history',
|
||||
'startedAt': now.toUtc().toIso8601String(),
|
||||
'endedAt': now.add(const Duration(minutes: 5)).toUtc().toIso8601String(),
|
||||
'totalActiveMs': 300000,
|
||||
'completed': true,
|
||||
'historySnapshotJson': '{"name":"remote-history-full"}',
|
||||
'minHeartRateBpm': 95,
|
||||
'averageHeartRateBpm': 125,
|
||||
'maxHeartRateBpm': 155,
|
||||
'totalDistanceMeters': 84,
|
||||
'totalCaloriesKcal': 24,
|
||||
'results': [
|
||||
{
|
||||
'id': 'remote-history-set-result',
|
||||
'workoutHistoryId': 'remote-history-full',
|
||||
'programSnapshotId': 'program-snapshot',
|
||||
'exerciseSnapshotId': 'exercise-snapshot-remote-exercise',
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'programNameSnapshot': 'Program',
|
||||
'exerciseNameSnapshot': 'Exercise',
|
||||
'timeEnabledSnapshot': false,
|
||||
'repsEnabledSnapshot': false,
|
||||
'scoreEnabledSnapshot': true,
|
||||
'scoreInputModeSnapshot': 'stopwatch',
|
||||
'actualScoreTimeMs': 12000,
|
||||
'sourceExerciseIdSnapshot': 'remote-exercise',
|
||||
'completedAt': now
|
||||
.add(const Duration(minutes: 1))
|
||||
.toUtc()
|
||||
.toIso8601String(),
|
||||
'status': 'completed',
|
||||
},
|
||||
],
|
||||
'stepResults': [
|
||||
{
|
||||
'id': 'remote-history-step-result',
|
||||
'workoutHistoryId': 'remote-history-full',
|
||||
'programSnapshotId': 'program-snapshot',
|
||||
'exerciseSnapshotId': 'exercise-snapshot-remote-exercise',
|
||||
'programIndex': 0,
|
||||
'exerciseIndex': 0,
|
||||
'setIndex': 0,
|
||||
'passageIndex': 0,
|
||||
'stepIndex': 0,
|
||||
'stepSnapshotId': 'step-snapshot',
|
||||
'stepNameSnapshot': 'Step',
|
||||
'stepTypeSnapshot': 'reps',
|
||||
'targetValueSnapshot': 10,
|
||||
'hasScoreSnapshot': false,
|
||||
'status': 'completed',
|
||||
'startedAt': now.toUtc().toIso8601String(),
|
||||
'completedAt': now
|
||||
.add(const Duration(seconds: 10))
|
||||
.toUtc()
|
||||
.toIso8601String(),
|
||||
'actualReps': 10,
|
||||
'sourceExerciseIdSnapshot': 'remote-exercise',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
final applied = await syncChangeRepository.applyRemoteItem(
|
||||
RemoteSyncedItem(
|
||||
resourceType: SyncResourceType.workoutHistory,
|
||||
clientId: 'remote-history-full',
|
||||
serverId: 'server-history-full',
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: now,
|
||||
serverUpdatedAt: now,
|
||||
deletedAt: null,
|
||||
payload: payload,
|
||||
),
|
||||
);
|
||||
|
||||
final restored = await historyRepository.findById('remote-history-full');
|
||||
|
||||
expect(applied, isTrue);
|
||||
expect(restored!.minHeartRateBpm, 95);
|
||||
expect(restored.averageHeartRateBpm, 125);
|
||||
expect(restored.maxHeartRateBpm, 155);
|
||||
expect(restored.totalDistanceMeters, 84);
|
||||
expect(restored.totalCaloriesKcal, 24);
|
||||
expect(restored.results.single.actualScoreTimeMs, 12000);
|
||||
expect(restored.stepResults.single.actualReps, 10);
|
||||
});
|
||||
|
||||
test('local sync pull defaults missing tags to empty lists', () async {
|
||||
final now = DateTime.utc(2026, 7, 22, 11);
|
||||
await syncChangeRepository.applyRemoteItem(
|
||||
@ -2616,8 +2821,14 @@ WorkoutHistory _history({
|
||||
required DateTime startedAt,
|
||||
WorkoutHistorySetResult? result,
|
||||
List<WorkoutHistorySetResult>? results,
|
||||
List<WorkoutHistoryStepResult> stepResults = const [],
|
||||
bool completed = true,
|
||||
int totalActiveMs = 300000,
|
||||
int? minHeartRateBpm,
|
||||
double? averageHeartRateBpm,
|
||||
int? maxHeartRateBpm,
|
||||
double? totalDistanceMeters,
|
||||
double? totalCaloriesKcal,
|
||||
}) {
|
||||
return WorkoutHistory(
|
||||
metadata: _metadata(id, startedAt),
|
||||
@ -2628,6 +2839,12 @@ WorkoutHistory _history({
|
||||
completed: completed,
|
||||
historySnapshotJson: '{"name":"$id"}',
|
||||
results: results ?? [result!],
|
||||
stepResults: stepResults,
|
||||
minHeartRateBpm: minHeartRateBpm,
|
||||
averageHeartRateBpm: averageHeartRateBpm,
|
||||
maxHeartRateBpm: maxHeartRateBpm,
|
||||
totalDistanceMeters: totalDistanceMeters,
|
||||
totalCaloriesKcal: totalCaloriesKcal,
|
||||
);
|
||||
}
|
||||
|
||||
@ -2744,6 +2961,7 @@ ExerciseStep _exerciseStep({
|
||||
String? scoreUnit,
|
||||
double? defaultTargetScore,
|
||||
int? defaultTargetScoreTimeMs,
|
||||
bool linkedToSeriesScore = false,
|
||||
}) {
|
||||
return ExerciseStep(
|
||||
id: id,
|
||||
@ -2757,6 +2975,7 @@ ExerciseStep _exerciseStep({
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
linkedToSeriesScore: linkedToSeriesScore,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
181
test/infrastructure/remote/share_api_test.dart
Normal file
181
test/infrastructure/remote/share_api_test.dart
Normal file
@ -0,0 +1,181 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gametime/application/application.dart';
|
||||
import 'package:gametime/domain/domain.dart';
|
||||
import 'package:gametime/infrastructure/remote/remote.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http/testing.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'sendShare maps local workout pack payload to server pack contract',
|
||||
() async {
|
||||
Map<String, Object?>? capturedBody;
|
||||
final api = HttpRemoteShareApi(
|
||||
HttpApiClient(
|
||||
baseUrl: Uri.parse('http://api.example.test'),
|
||||
client: MockClient((request) async {
|
||||
capturedBody = _jsonMap(request.body);
|
||||
expect(request.method, 'POST');
|
||||
expect(request.url.path, '/shares');
|
||||
expect(request.headers['authorization'], 'Bearer token-1');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'shareId': 'share-1',
|
||||
'recipientUserIds': ['user-2'],
|
||||
'unresolvedEmails': const <String>[],
|
||||
}),
|
||||
201,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
await api.sendShare(
|
||||
resourceType: ShareResourceType.pack,
|
||||
payload: const {
|
||||
'name': 'Pack reprise',
|
||||
'workouts': [
|
||||
{'id': 'template-1', 'name': 'Séance 1'},
|
||||
{'id': 'template-2', 'name': 'Séance 2'},
|
||||
],
|
||||
},
|
||||
recipientEmails: const ['friend@example.com'],
|
||||
token: 'token-1',
|
||||
);
|
||||
|
||||
expect(capturedBody, {
|
||||
'shareKind': 'pack',
|
||||
'packName': 'Pack reprise',
|
||||
'items': [
|
||||
{
|
||||
'resourceType': 'workoutTemplate',
|
||||
'payload': {'id': 'template-1', 'name': 'Séance 1'},
|
||||
},
|
||||
{
|
||||
'resourceType': 'workoutTemplate',
|
||||
'payload': {'id': 'template-2', 'name': 'Séance 2'},
|
||||
},
|
||||
],
|
||||
'recipientEmails': ['friend@example.com'],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'fetchInbox maps server pack payload without resourceType to local pack',
|
||||
() async {
|
||||
final api = HttpRemoteShareApi(
|
||||
HttpApiClient(
|
||||
baseUrl: Uri.parse('http://api.example.test'),
|
||||
client: MockClient((request) async {
|
||||
expect(request.method, 'GET');
|
||||
expect(request.url.path, '/shares/inbox');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'items': [
|
||||
{
|
||||
'shareId': 'share-pack-1',
|
||||
'senderUserId': 'sender-1',
|
||||
'shareKind': 'pack',
|
||||
'packName': 'Pack été',
|
||||
'resourceType': null,
|
||||
'payload': {
|
||||
'items': [
|
||||
{
|
||||
'resourceType': 'workoutTemplate',
|
||||
'payload': {'id': 'template-1', 'name': 'Séance 1'},
|
||||
},
|
||||
],
|
||||
},
|
||||
'status': 'pending',
|
||||
'createdAt': '2026-07-17T12:00:00.000Z',
|
||||
'respondedAt': null,
|
||||
},
|
||||
],
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
final items = await api.fetchInbox('token-1');
|
||||
final payload = _jsonMap(items.single.payloadJson);
|
||||
|
||||
expect(items.single.resourceType, ShareResourceType.pack);
|
||||
expect(payload, {
|
||||
'name': 'Pack été',
|
||||
'workouts': [
|
||||
{'id': 'template-1', 'name': 'Séance 1'},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'acceptShare returns every created resource from server packs',
|
||||
() async {
|
||||
final api = HttpRemoteShareApi(
|
||||
HttpApiClient(
|
||||
baseUrl: Uri.parse('http://api.example.test'),
|
||||
client: MockClient((request) async {
|
||||
expect(request.method, 'POST');
|
||||
expect(request.url.path, '/shares/share-pack-1/accept');
|
||||
return http.Response(
|
||||
jsonEncode({
|
||||
'createdResources': [
|
||||
_createdResourceJson(
|
||||
resourceType: 'program',
|
||||
clientId: 'program-1',
|
||||
serverId: 'server-program-1',
|
||||
),
|
||||
_createdResourceJson(
|
||||
resourceType: 'workoutTemplate',
|
||||
clientId: 'template-1',
|
||||
serverId: 'server-template-1',
|
||||
),
|
||||
],
|
||||
}),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
final resources = await api.acceptShare('share-pack-1', 'token-1');
|
||||
|
||||
expect(resources.map((item) => item.resourceType), [
|
||||
SyncResourceType.program,
|
||||
SyncResourceType.workoutTemplate,
|
||||
]);
|
||||
expect(resources.map((item) => item.clientId), [
|
||||
'program-1',
|
||||
'template-1',
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object?> _createdResourceJson({
|
||||
required String resourceType,
|
||||
required String clientId,
|
||||
required String serverId,
|
||||
}) {
|
||||
return {
|
||||
'resourceType': resourceType,
|
||||
'clientId': clientId,
|
||||
'serverId': serverId,
|
||||
'schemaVersion': 1,
|
||||
'clientUpdatedAt': '2026-07-17T12:00:00.000Z',
|
||||
'serverUpdatedAt': '2026-07-17T12:01:00.000Z',
|
||||
'deletedAt': null,
|
||||
'payload': {'id': clientId},
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Object?> _jsonMap(String source) {
|
||||
final decoded = jsonDecode(source);
|
||||
return Map<String, Object?>.from(decoded as Map);
|
||||
}
|
||||
Reference in New Issue
Block a user