dwRepoLocalReadsConformance function
void
dwRepoLocalReadsConformance(
- String description, {
- required Future<
DwRepoLocalReadsFixture> createFixture(),
The tests every DwRepoLocalReads implementation must pass.
Call it from the implementation's own test suite:
import 'package:dartway_serverpod_core_flutter/testing.dart';
void main() {
dwRepoLocalReadsConformance(
'MyLocalReads',
createFixture: () async => MyFixture(await openStore()),
);
}
Keeping a read is a write, and it carries the same danger as any other: a
snapshot committed after the user signed out survives the purge that was
supposed to remove it. keep hands the store a transaction and lets the
core write the order of operations inside it, which is what stops an
implementation from checking the binding and committing the row separately —
but nothing in the type system says the transaction is real, that
isBindingCurrent reads inside it, or that a rolled-back body leaves
nothing behind. This runs instead.
Implementation
void dwRepoLocalReadsConformance(
String description, {
required Future<DwRepoLocalReadsFixture> Function() createFixture,
}) {
group('$description — DwRepoLocalReads conformance', () {
late DwRepoLocalReadsFixture fixture;
setUp(() async => fixture = await createFixture());
tearDown(() => fixture.dispose());
test('an active binding resolves and reports itself current', () async {
final binding = await fixture.store.resolveBinding();
expect(
binding,
isNotNull,
reason: 'The fixture must start with a usable session.',
);
expect(await fixture.store.isBindingCurrent(binding!), isTrue);
});
test('a revoked binding is never current again', () async {
final binding = (await fixture.store.resolveBinding())!;
binding.invalidate();
expect(await fixture.store.isBindingCurrent(binding), isFalse);
});
test('keep returns what the body returned', () async {
final result = await fixture.store.keep<String>((_) async => 'returned');
expect(result, 'returned');
});
test('a body that throws commits nothing', () async {
final binding = (await fixture.store.resolveBinding())!;
final kept = fixture.keptQueryFor(binding);
await expectLater(
fixture.store.keep<void>((tx) async {
await tx.storeSnapshot(
queryKey: kept.queryKey,
snapshot: kept.snapshot,
);
throw const _ConformanceFailure();
}),
throwsA(isA<_ConformanceFailure>()),
);
expect(
await fixture.keptStorageKeysFor(binding.scope),
isEmpty,
reason:
'A snapshot written inside a failed transaction must roll back '
'with it. A store that commits the row before the body returns '
'keeps a read nobody asked it to keep.',
);
});
test('an accepted snapshot is durably kept and readable back', () async {
final binding = (await fixture.store.resolveBinding())!;
final kept = fixture.keptQueryFor(binding);
final accepted = await fixture.store.keep<bool>((tx) async {
if (!await tx.isBindingCurrent(binding)) return false;
return tx.storeSnapshot(
queryKey: kept.queryKey,
snapshot: kept.snapshot,
);
});
expect(accepted, isTrue);
expect(await fixture.keptStorageKeysFor(binding.scope), [
kept.queryKey.toStorageKey(),
]);
final loaded = await fixture.store.loadSnapshot(
binding: binding,
queryKey: kept.queryKey,
);
expect(loaded, isNotNull);
expect(loaded!.scope, binding.scope);
expect(loaded.schemaVersion, kept.snapshot.schemaVersion);
});
test('declining a query keeps nothing and is not a failure', () async {
final binding = (await fixture.store.resolveBinding())!;
final ignoredQuery = fixture.ignoredQueryFor(binding);
if (ignoredQuery == null) {
markTestSkipped('This store keeps every query it is offered.');
return;
}
final kept = fixture.keptQueryFor(binding);
final accepted = await fixture.store.keep<bool>(
(tx) => tx.storeSnapshot(
queryKey: ignoredQuery,
snapshot: kept.snapshot,
),
);
expect(accepted, isFalse);
expect(await fixture.keptStorageKeysFor(binding.scope), isEmpty);
});
test('a snapshot offered under a revoked binding is refused', () async {
final binding = (await fixture.store.resolveBinding())!;
final kept = fixture.keptQueryFor(binding);
await fixture.signOut();
final refused = await fixture.store.keep<bool>((tx) async {
if (!await tx.isBindingCurrent(binding)) return false;
await tx.storeSnapshot(
queryKey: kept.queryKey,
snapshot: kept.snapshot,
);
return true;
});
expect(refused, isFalse);
expect(await fixture.keptStorageKeysFor(binding.scope), isEmpty);
});
// The case the inversion exists for. A store that reads the binding outside
// the transaction says "current", the sign-out commits, the row commits
// after it, and the device is left holding a snapshot of a session that
// ended — past the purge that was supposed to take it.
//
// The interleaving is the store's to order and both orders are correct.
// What this pins is that whichever it picks, the answer and the disk say
// the same thing afterwards.
test('a sign-out landing mid-transaction leaves no torn state', () async {
final binding = (await fixture.store.resolveBinding())!;
final kept = fixture.keptQueryFor(binding);
late Future<void> signOut;
final accepted = await fixture.store.keep<bool>((tx) async {
if (!await tx.isBindingCurrent(binding)) return false;
signOut = fixture.signOut();
return tx.storeSnapshot(
queryKey: kept.queryKey,
snapshot: kept.snapshot,
);
});
await signOut;
final held = await fixture.keptStorageKeysFor(binding.scope);
expect(
held,
accepted ? [kept.queryKey.toStorageKey()] : isEmpty,
reason:
'The answer and the disk disagree: keep returned $accepted while '
'the store holds $held. The binding check has to read inside the '
'same transaction that commits the row — a check taken before '
'opening it is exactly this bug.',
);
});
});
}