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
4 changed files with 47595 additions and 1 deletions
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_meditate_server/src/meditate_base.dart';
|
||||
import 'package:halkyon_meditate_server/src/pruning.dart';
|
||||
import 'package:serverpod/protocol.dart' show FutureCallEntry;
|
||||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'support/meditate_fixtures.dart';
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
"Meditate's server",
|
||||
// Signing in runs concurrent transactions (the rate limit and the new
|
||||
// account), which the test's database rollback can't wrap. So these
|
||||
// tests don't roll back; they delete what they create instead.
|
||||
rollbackDatabase: RollbackDatabase.disabled,
|
||||
(sessionBuilder, endpoints) {
|
||||
late Serverpod pod;
|
||||
|
||||
setUp(() async {
|
||||
useMeditateBase(sessionBuilder);
|
||||
pod = sessionBuilder.build().serverpod;
|
||||
await _deleteAll(sessionBuilder.build());
|
||||
});
|
||||
tearDown(() => _deleteAll(sessionBuilder.build()));
|
||||
|
||||
/// Calls [method] of the module's endpoint [endpoint] as a request
|
||||
/// reaches it: through the server's own dispatch, under the name the
|
||||
/// app's client calls (`halkyon_base.<endpoint>`).
|
||||
Future<Object?> call(
|
||||
String endpoint,
|
||||
String method, [
|
||||
Map<String, Object?> parameters = const {},
|
||||
]) {
|
||||
final connector = pod.endpoints.getConnectorByName(
|
||||
'halkyon_base.$endpoint',
|
||||
);
|
||||
final methodConnector =
|
||||
connector!.methodConnectors[method]! as MethodConnector;
|
||||
return methodConnector.call(sessionBuilder.build(), parameters);
|
||||
}
|
||||
|
||||
test('its basics name the app it serves, `meditate`, so only Meditate '
|
||||
'takes it for an instance of its own', () async {
|
||||
final basics = (await call('instance', 'basics'))! as InstanceBasics;
|
||||
|
||||
expect(basics.app, 'meditate');
|
||||
expect(basics.minimumAppBuild, 1);
|
||||
expect(
|
||||
basics.sourceCodeUrl,
|
||||
'https://code.halkyon.app/project-halkyon/halkyon',
|
||||
);
|
||||
});
|
||||
|
||||
test('makes no account without a way in: the module signs nobody in '
|
||||
'anonymously (ADR 0020, D25)', () {
|
||||
expect(
|
||||
pod.endpoints.getConnectorByName('halkyon_base.anonymousIdp'),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
pod.endpoints.getConnectorByName('halkyon_base.instance'),
|
||||
isNotNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('syncs for people signed in alone: the sync engine refuses a '
|
||||
'call without a session (ADR 0038, D7)', () {
|
||||
final sync = pod.endpoints.getConnectorByName(
|
||||
'serverpod_offline_sync.offlineSync',
|
||||
);
|
||||
expect(sync, isNotNull);
|
||||
expect(sync!.endpoint.requireLogin, isTrue);
|
||||
});
|
||||
|
||||
test("is a member of the instance's register the operator names, and "
|
||||
'holds the register otherwise (ADR 0016)', () {
|
||||
expect(
|
||||
meditateBase(
|
||||
accounts: AccountsMode.fromEnvironment(const {
|
||||
'ACCOUNT_REGISTER_URL': 'http://learn:8082',
|
||||
}),
|
||||
).accounts,
|
||||
isA<MemberOfRegister>().having(
|
||||
(member) => member.register,
|
||||
'register',
|
||||
Uri.parse('http://learn:8082'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
meditateBase(accounts: AccountsMode.fromEnvironment(const {}))
|
||||
.accounts,
|
||||
isA<HoldsRegister>(),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'the pruning is scheduled once, however often the server starts, '
|
||||
'and the one under its earlier name goes',
|
||||
() async {
|
||||
// INSERT INTO "serverpod_future_call" (…) VALUES (…)
|
||||
await FutureCallEntry.db.insertRow(
|
||||
sessionBuilder.build(),
|
||||
FutureCallEntry(
|
||||
name: 'PruningFutureCall.pruneAnonymousPeople',
|
||||
time: DateTime.utc(2026, 10, 25, 3, 17),
|
||||
serverId: 'default',
|
||||
identifier: 'prune-abandoned-anonymous-people',
|
||||
),
|
||||
);
|
||||
await schedulePruning(pod);
|
||||
await schedulePruning(pod);
|
||||
|
||||
final scheduled = await FutureCallEntry.db.find(
|
||||
sessionBuilder.build(),
|
||||
where: (t) => t.identifier.inSet(pruningIdentifiers.toSet()),
|
||||
);
|
||||
expect(
|
||||
scheduled.map((entry) => entry.identifier),
|
||||
unorderedEquals(pruningIdentifiers),
|
||||
);
|
||||
expect(
|
||||
await FutureCallEntry.db.count(
|
||||
sessionBuilder.build(),
|
||||
where: (t) =>
|
||||
t.identifier.equals('prune-abandoned-anonymous-people'),
|
||||
),
|
||||
0,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Deletes every rate-limit record, every person (their sessions go with
|
||||
/// them, `ON DELETE CASCADE`) and every future call.
|
||||
Future<void> _deleteAll(Session session) async {
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
await AuthUser.db.deleteWhere(session, where: (t) => t.id.notEquals(null));
|
||||
await FutureCallEntry.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:halkyon_base_server/base.dart';
|
||||
import 'package:halkyon_meditate_server/src/meditate_base.dart';
|
||||
import 'package:serverpod_auth_idp_server/core.dart';
|
||||
|
||||
import '../test_tools/serverpod_test_tools.dart';
|
||||
|
||||
/// Sets the server up as it runs (lib/server.dart): halkyon_base as Meditate
|
||||
/// configures it (ADR 0014), and authentication. On the pod [sessionBuilder]'s
|
||||
/// sessions run on, not on `Serverpod.instance`: that is the pod made last.
|
||||
void useMeditateBase(TestSessionBuilder sessionBuilder) {
|
||||
final pod = sessionBuilder.build().serverpod;
|
||||
HalkyonBase.configure(pod, meditateBase());
|
||||
AuthServices.set(
|
||||
tokenManagerBuilders: tokenManagerBuilders,
|
||||
identityProviderBuilders: identityProviderBuilders(pod),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
||||
/* To generate run: "serverpod generate" */
|
||||
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: library_private_types_in_public_api
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: type_literal_in_constant_pattern
|
||||
// ignore_for_file: use_super_parameters
|
||||
// ignore_for_file: invalid_use_of_internal_member
|
||||
// ignore_for_file: no_leading_underscores_for_local_identifiers
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:io' as _idi;
|
||||
import 'package:halkyon_meditate_server/src/generated/future_calls.dart'
|
||||
as _iluznlp5;
|
||||
import 'package:serverpod/serverpod.dart' as _is;
|
||||
import 'package:serverpod_test/serverpod_test.dart' as _ist;
|
||||
import 'package:halkyon_meditate_server/src/generated/protocol.dart';
|
||||
import 'package:halkyon_meditate_server/src/generated/endpoints.dart';
|
||||
export 'package:serverpod_test/serverpod_test_public_exports.dart';
|
||||
|
||||
/// Creates a new test group that takes a callback that can be used to write tests.
|
||||
/// The callback has two parameters: `sessionBuilder` and `endpoints`.
|
||||
/// `sessionBuilder` is used to build a `Session` object that represents the server state during an endpoint call and is used to set up scenarios.
|
||||
/// `endpoints` contains all your Serverpod endpoints and lets you call them:
|
||||
/// ```dart
|
||||
/// withServerpod('Given Example endpoint', (sessionBuilder, endpoints) {
|
||||
/// test('when calling `hello` then should return greeting', () async {
|
||||
/// final greeting = await endpoints.example.hello(sessionBuilder, 'Michael');
|
||||
/// expect(greeting, 'Hello Michael');
|
||||
/// });
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// **Configuration options**
|
||||
///
|
||||
/// [applyMigrations] Whether pending migrations should be applied when starting Serverpod. Defaults to `true`
|
||||
///
|
||||
/// [enableSessionLogging] Whether session logging should be enabled. Defaults to `false`
|
||||
///
|
||||
/// [ephemeralDatabase] Whether this group gets its own empty database, created when the group starts and dropped when it finishes. Defaults to `true`.
|
||||
/// Set this to `false` to use the database configured for [runMode], including a previously seeded database. [configOverride] can still replace that database. The configured database is not created or dropped, so groups that share it cannot run in parallel. [rollbackDatabase] still controls whether writes inside the group are rolled back.
|
||||
///
|
||||
/// [rollbackDatabase] Options for when to rollback the database during the test lifecycle.
|
||||
/// By default `withServerpod` does all database operations inside a transaction that is rolled back after each `test` case.
|
||||
/// Just like the following enum describes, the behavior of the automatic rollbacks can be configured:
|
||||
/// ```dart
|
||||
/// /// Options for when to rollback the database during the test lifecycle.
|
||||
/// enum RollbackDatabase {
|
||||
/// /// After each test. This is the default.
|
||||
/// afterEach,
|
||||
///
|
||||
/// /// After all tests.
|
||||
/// afterAll,
|
||||
///
|
||||
/// /// Disable rolling back the database.
|
||||
/// disabled,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [runMode] The run mode that Serverpod should be running in. Defaults to `test`.
|
||||
///
|
||||
/// [serverpodLoggingMode] The logging mode used when creating Serverpod. Defaults to `ServerpodLoggingMode.normal`
|
||||
///
|
||||
/// [serverpodStartTimeout] The timeout to use when starting Serverpod, which connects to the database among other things. Defaults to `Duration(seconds: 120)`.
|
||||
///
|
||||
/// [testServerOutputMode] Options for controlling test server output during test execution. Defaults to `TestServerOutputMode.normal`.
|
||||
/// ```dart
|
||||
/// /// Options for controlling test server output during test execution.
|
||||
/// enum TestServerOutputMode {
|
||||
/// /// Default mode - only stderr is printed (stdout suppressed).
|
||||
/// /// This hides normal startup/shutdown logs while preserving error messages.
|
||||
/// normal,
|
||||
///
|
||||
/// /// All logging - both stdout and stderr are printed.
|
||||
/// /// Useful for debugging when you need to see all server output.
|
||||
/// verbose,
|
||||
///
|
||||
/// /// No logging - both stdout and stderr are suppressed.
|
||||
/// /// Completely silent mode, useful when you don't want any server output.
|
||||
/// silent,
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [configOverride] A function to override the server configuration. This function is called with
|
||||
/// the default server configuration after it is loaded from the config/ directory
|
||||
/// and before it is used to start the server. Use this to override particular
|
||||
/// settings in the server configuration.
|
||||
///
|
||||
/// [databaseInterceptor] Optional interceptor that replaces the default database for each session.
|
||||
/// See [Serverpod.databaseInterceptor] for more information.
|
||||
///
|
||||
/// [testGroupTagsOverride] By default Serverpod test tools tags the `withServerpod` test group with `"integration"`.
|
||||
/// This is to provide a simple way to only run unit or integration tests.
|
||||
/// This property allows this tag to be overridden to something else. Defaults to `['integration']`.
|
||||
///
|
||||
/// [experimentalFeatures] Optionally specify experimental features. See [Serverpod] for more information.
|
||||
///
|
||||
/// [serverDirectory] The server package directory `config/<runMode>.yaml`, `config/passwords.yaml`,
|
||||
/// and `migrations/<module>/...` are resolved against. Defaults to
|
||||
/// [Directory.current] at the time the test boots. Pass this when the test
|
||||
/// isolate's cwd is not the server package root (e.g. running tests from a
|
||||
/// workspace parent directory) so config and migrations are still loaded
|
||||
/// from the right place.
|
||||
@_ist.isTestGroup
|
||||
void withServerpod(
|
||||
String testGroupName,
|
||||
_ist.TestClosure<TestEndpoints> testClosure, {
|
||||
bool? applyMigrations,
|
||||
_is.ServerpodConfig Function(_is.ServerpodConfig)? configOverride,
|
||||
_is.DatabaseInterceptor? databaseInterceptor,
|
||||
bool? enableSessionLogging,
|
||||
bool? ephemeralDatabase,
|
||||
_is.ExperimentalFeatures? experimentalFeatures,
|
||||
_ist.RollbackDatabase? rollbackDatabase,
|
||||
String? runMode,
|
||||
_is.RuntimeParametersListBuilder? runtimeParametersBuilder,
|
||||
_idi.Directory? serverDirectory,
|
||||
_is.ServerpodLoggingMode? serverpodLoggingMode,
|
||||
Duration? serverpodStartTimeout,
|
||||
List<String>? testGroupTagsOverride,
|
||||
_ist.TestServerOutputMode? testServerOutputMode,
|
||||
}) {
|
||||
_ist.buildWithServerpod<_InternalTestEndpoints>(
|
||||
testGroupName,
|
||||
_ist.TestServerpod(
|
||||
testEndpoints: _InternalTestEndpoints(),
|
||||
endpoints: Endpoints(),
|
||||
serializationManager: Protocol(),
|
||||
runMode: runMode,
|
||||
applyMigrations: applyMigrations,
|
||||
ephemeralDatabase: ephemeralDatabase,
|
||||
isDatabaseEnabled: true,
|
||||
serverpodLoggingMode: serverpodLoggingMode,
|
||||
testServerOutputMode: testServerOutputMode,
|
||||
serverDirectory: serverDirectory,
|
||||
experimentalFeatures: experimentalFeatures,
|
||||
configOverride: configOverride,
|
||||
runtimeParametersBuilder: runtimeParametersBuilder,
|
||||
databaseInterceptor: databaseInterceptor,
|
||||
),
|
||||
maybeRollbackDatabase: rollbackDatabase,
|
||||
maybeEnableSessionLogging: enableSessionLogging,
|
||||
maybeTestGroupTagsOverride: testGroupTagsOverride,
|
||||
maybeServerpodStartTimeout: serverpodStartTimeout,
|
||||
maybeTestServerOutputMode: testServerOutputMode,
|
||||
)(testClosure);
|
||||
}
|
||||
|
||||
class TestEndpoints {
|
||||
late final futureCalls = _FutureCalls();
|
||||
}
|
||||
|
||||
class _InternalTestEndpoints extends TestEndpoints
|
||||
implements _ist.InternalTestEndpoints {
|
||||
@override
|
||||
void initialize(
|
||||
_is.SerializationManager serializationManager,
|
||||
_is.EndpointDispatch endpoints,
|
||||
) {}
|
||||
}
|
||||
|
||||
class _FutureCalls {
|
||||
late final pruning = _PruningFutureCall();
|
||||
}
|
||||
|
||||
class _PruningFutureCall {
|
||||
Future<void> pruneAccounts(_ist.TestSessionBuilder sessionBuilder) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _iluznlp5.PruningPruneAccountsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> forgetSignUpAttempts(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _iluznlp5.PruningForgetSignUpAttemptsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tellMembersOfDeletions(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _iluznlp5.PruningTellMembersOfDeletionsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> forgetDeletedAccounts(
|
||||
_ist.TestSessionBuilder sessionBuilder,
|
||||
) async {
|
||||
var _localUniqueSession =
|
||||
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
|
||||
try {
|
||||
await _iluznlp5.PruningForgetDeletedAccountsFutureCall().invoke(
|
||||
_localUniqueSession,
|
||||
null,
|
||||
);
|
||||
} finally {
|
||||
await _localUniqueSession.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue