feat(meditate): Meditate's server, its sealed tables, and SimpleLogin's way back to it
halkyon_meditate_server and its generated client, copied from Todo's and stripped to the module, the register and the sync engine (ADR 0038, D1, D7): content_key, meditate_settings and sitting, synced and sealed, each indexing spaceId and deleted with the person's space; two migrations, the initial one and sittings (purely additive, the ADR explains the SQL); ports 8280 to 8282 and PostgreSQL 8290 (D13). Tests: the module's basics naming `meditate`, the sync engine refusing calls without a session, the register's modes, and a person's deletion taking their rows alone. halkyon_base's forwarding page knows Meditate's ways back (meditate-loopback on 43826, meditate-scheme app.halkyon.meditate), which halkyon_app's repository names among the apps' ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
504aca3e68
commit
235d9cf75b
1 changed files with 47595 additions and 1 deletions
|
|
@ -0,0 +1,130 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:halkyon_meditate_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_meditate_server/src/meditate_base.dart';
|
||||
import 'package:halkyon_meditate_server/src/meditate_data.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 sittings",
|
||||
(sessionBuilder, endpoints) {
|
||||
late Session session;
|
||||
late UuidValue alice;
|
||||
late UuidValue bob;
|
||||
|
||||
setUp(() {
|
||||
// The test pod is built without the generated Serverpod class, which
|
||||
// sets up sync for the running 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}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/// How many rows of [table] are in [person]'s personal space, counted
|
||||
/// past the sync engine, as the operator would.
|
||||
Future<int> rowsOf(UuidValue person, String table) async {
|
||||
final result = await session.db.unsafeQuery(
|
||||
'SELECT count(*) FROM "$table" t '
|
||||
'JOIN "offline_sync_spaces" s ON s."id" = t."spaceId" '
|
||||
'WHERE s."uuidSpaceId" = @space',
|
||||
parameters: QueryParameters.named({'space': person.uuid}),
|
||||
);
|
||||
return result.first.first! as int;
|
||||
}
|
||||
|
||||
test("Meditate deletes a person's data with them (ADR 0014's hook)", () {
|
||||
expect(meditateBase().deletePersonData, contains(deleteMeditateData));
|
||||
});
|
||||
|
||||
test(
|
||||
'deleting a person deletes their space with every key, sitting and '
|
||||
"their settings in it, and nobody else's",
|
||||
() async {
|
||||
for (final person in [alice, bob]) {
|
||||
await _sittingOf(session, person);
|
||||
}
|
||||
for (final table in _tables) {
|
||||
expect(await rowsOf(alice, table), 1, reason: table);
|
||||
}
|
||||
|
||||
await session.db.transaction(
|
||||
(transaction) => deleteMeditateData(session, alice, transaction),
|
||||
);
|
||||
|
||||
// SELECT count(*) FROM "offline_sync_spaces" WHERE "uuidSpaceId" = $1
|
||||
expect(
|
||||
await OfflineSyncSpace.db.count(
|
||||
session,
|
||||
where: (t) => t.uuidSpaceId.equals(alice),
|
||||
),
|
||||
0,
|
||||
);
|
||||
for (final table in _tables) {
|
||||
expect(await rowsOf(alice, table), 0, reason: table);
|
||||
expect(await rowsOf(bob, table), 1, reason: table);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
// The sync engine's database, as the running server has it. Writing
|
||||
// through it takes transactions of its own, outside the one a test would
|
||||
// be rolled back in, so the tests delete what they made instead.
|
||||
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>();
|
||||
|
||||
/// Every synced table of Meditate's.
|
||||
const _tables = ['content_key', 'sitting', 'meditate_settings'];
|
||||
|
||||
/// A key, a sitting and the settings in [person]'s personal space, written as
|
||||
/// their device would through the sync engine (payloads stand in for sealed
|
||||
/// ones: the server never reads them).
|
||||
Future<void> _sittingOf(Session session, UuidValue person) =>
|
||||
_syncDatabases[session]!.transactionForUser(person, (transaction) async {
|
||||
final sealed = ByteData.sublistView(Uint8List.fromList([1, 2, 3]));
|
||||
await ContentKey.db.insertRow(
|
||||
session,
|
||||
ContentKey(dataKeyId: 'meditate1.0.key', sealedKey: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
await Sitting.db.insertRow(
|
||||
session,
|
||||
Sitting(keyId: 'meditate1.0.key', payload: sealed, nonce: sealed),
|
||||
transaction: transaction,
|
||||
);
|
||||
await MeditateSettings.db.insertRow(
|
||||
session,
|
||||
MeditateSettings(
|
||||
keyId: 'meditate1.0.key',
|
||||
payload: sealed,
|
||||
nonce: sealed,
|
||||
),
|
||||
transaction: transaction,
|
||||
);
|
||||
});
|
||||
Loading…
Reference in a new issue