Merge branch 'train-days' into integration

# Conflicts:
#	apps/train/halkyon_train_flutter/lib/src/app/train_paths.dart
#	docs/adr/0030-halkyon-train.md
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-03 23:14:02 +01:00
commit 81adeb91cc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 25705 additions and 410 deletions

View file

@ -0,0 +1,116 @@
import 'dart:typed_data';
import 'package:halkyon_train_server/src/generated/protocol.dart';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
import 'package:test/test.dart';
import '../test_tools/serverpod_test_tools.dart';
void main() {
withServerpod(
"A person's weights (ADR 0030, D64)",
(sessionBuilder, endpoints) {
late Session session;
late UuidValue alice;
late UuidValue bob;
final sealed = ByteData.sublistView(Uint8List.fromList([1, 2, 3]));
setUp(() {
// As person_deletion_test: the test pod is built without the
// generated Serverpod class, which sets up sync for the server.
Serverpod.instance.initializeOfflineSync(syncTables: syncTables);
session = sessionBuilder.build();
alice = const Uuid().v7obj();
bob = const Uuid().v7obj();
});
tearDown(() async {
for (final space in [alice, bob]) {
await session.db.unsafeExecute(
'DELETE FROM "offline_sync_spaces" WHERE "uuidSpaceId" = @space',
parameters: QueryParameters.named({'space': space.uuid}),
);
}
});
OfflineSyncDatabase sync() => _syncDatabases[session]!;
/// Writes a weight in [person]'s own space, as their device's sync
/// does.
Future<BodyWeight> weightOf(UuidValue person) =>
sync().transactionForUser(
person,
(transaction) => BodyWeight.db.insertRow(
session,
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
transaction: transaction,
),
);
test(
"is written in the person's own space and read back there",
() async {
final written = await weightOf(alice);
final read = await sync().transactionForUser(
alice,
(transaction) =>
BodyWeight.db.find(session, transaction: transaction),
);
expect([for (final weight in read) weight.id], [written.id]);
},
);
test("is denied to anyone else: someone else's reading finds none of "
'it', () async {
await weightOf(alice);
final read = await sync().transactionForUser(
bob,
(transaction) =>
BodyWeight.db.find(session, transaction: transaction),
);
expect(read, isEmpty);
});
test("is denied to anyone else: writing in another person's space is "
'refused before anything is written', () async {
await expectLater(
sync().transactionForUser(
bob,
spaceId: alice,
(transaction) => BodyWeight.db.insertRow(
session,
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
transaction: transaction,
),
),
throwsA(isA<OfflineSyncSpaceMembershipException>()),
);
// Nothing landed in Alice's space.
final rows = await session.db.unsafeQuery(
'SELECT count(*) FROM "body_weight" t '
'JOIN "offline_sync_spaces" s ON s."id" = t."spaceId" '
'WHERE s."uuidSpaceId" = @space',
parameters: QueryParameters.named({'space': alice.uuid}),
);
expect(rows.first.first, 0);
});
},
// As person_deletion_test: the sync engine's database, writing in
// transactions of its own, so the tests delete what they made.
databaseInterceptor: (session, inner) {
final database = offlineSyncDatabaseInterceptor(session, inner);
if (database is OfflineSyncDatabase) {
_syncDatabases[session] = database;
}
return database;
},
rollbackDatabase: RollbackDatabase.disabled,
);
}
/// The sync engine's database of each session, as the test pod's own
/// database wraps it.
final _syncDatabases = Expando<OfflineSyncDatabase>();

View file

@ -53,8 +53,8 @@ void main() {
test(
'deleting a person deletes their space with every key, workout, '
"series, profile, day, day's series, night, plan, route and training "
"place in it, and nobody else's",
"series, profile, day, day's series, night, plan, route, training "
"place, strength plan and weight in it, and nobody else's",
() async {
for (final person in [alice, bob]) {
await _workoutOf(session, person);
@ -71,6 +71,7 @@ void main() {
'route',
'training_place',
'strength_plan',
'body_weight',
]) {
expect(await rowsOf(alice, table), 1, reason: table);
}
@ -99,6 +100,7 @@ void main() {
'route',
'training_place',
'strength_plan',
'body_weight',
]) {
expect(await rowsOf(alice, table), 0, reason: table);
expect(await rowsOf(bob, table), 1, reason: table);
@ -196,4 +198,9 @@ Future<void> _workoutOf(Session session, UuidValue person) =>
StrengthPlan(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
transaction: transaction,
);
await BodyWeight.db.insertRow(
session,
BodyWeight(keyId: 'p1.0.key', payload: sealed, nonce: sealed),
transaction: transaction,
);
});