dwRepoLocalWritesConformance function
void
dwRepoLocalWritesConformance(
- String description, {
- required Future<
DwRepoLocalWritesFixture> createFixture(),
The tests every DwRepoLocalWrites implementation must pass.
Call it from the implementation's own test suite:
import 'package:dartway_serverpod_core_flutter/testing.dart';
void main() {
dwRepoLocalWritesConformance(
'DwOfflineLocalWrites',
createFixture: () async => MyFixture(await openStore()),
);
}
It exists because the dangerous part of this contract cannot be held by the
shape of the API. write hands the store a transaction and lets the core
write what happens inside it, which is what stops an implementation from
checking the binding and enqueueing in two separate commits — 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. Prose asking
for it compiles just as well when ignored. This runs instead.
Implementation
void dwRepoLocalWritesConformance(
String description, {
required Future<DwRepoLocalWritesFixture> Function() createFixture,
}) {
group('$description — DwRepoLocalWrites conformance', () {
late DwRepoLocalWritesFixture 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('write returns what the body returned', () async {
final result = await fixture.store.write<String>((_) async => 'returned');
expect(result, 'returned');
});
test('a body that throws commits nothing', () async {
final binding = (await fixture.store.resolveBinding())!;
final mutation = fixture.mutationFor(binding);
await expectLater(
fixture.store.write<void>((tx) async {
await tx.enqueue(mutation);
throw const _ConformanceFailure();
}),
throwsA(isA<_ConformanceFailure>()),
);
expect(
await fixture.queuedFor(binding.scope),
isEmpty,
reason:
'An enqueue inside a failed transaction must roll back with it. '
'A store that writes the row before the body returns keeps a '
'mutation nobody asked to keep.',
);
});
test('an accepted enqueue is durably queued', () async {
final binding = (await fixture.store.resolveBinding())!;
final mutation = fixture.mutationFor(binding);
final outcome = await fixture.store.write<DwRepoEnqueue>((tx) async {
if (!await tx.isBindingCurrent(binding)) return DwRepoEnqueue.stale;
await tx.enqueue(mutation);
return DwRepoEnqueue.accepted;
});
expect(outcome, DwRepoEnqueue.accepted);
expect(
(await fixture.queuedFor(binding.scope)).map((m) => m.mutationId),
[mutation.mutationId],
);
});
test('a mutation enqueued under a revoked binding is refused', () async {
final binding = (await fixture.store.resolveBinding())!;
final mutation = fixture.mutationFor(binding);
await fixture.signOut();
final outcome = await fixture.store.write<DwRepoEnqueue>((tx) async {
if (!await tx.isBindingCurrent(binding)) return DwRepoEnqueue.stale;
await tx.enqueue(mutation);
return DwRepoEnqueue.accepted;
});
expect(outcome, DwRepoEnqueue.stale);
expect(await fixture.queuedFor(binding.scope), isEmpty);
});
// The case the whole inversion exists for. The network drops, the write
// goes to the queue, and the user signs out in that exact window. A store
// that reads the binding outside the transaction says "current", the
// sign-out commits, and the enqueue commits after it — leaving the queue
// and the answer disagreeing about whether anything was written.
//
// The interleaving itself is the store's to order, and both orders are
// correct. What this pins is that whichever it picks, the outcome and the
// queue say the same thing afterwards.
test('a sign-out landing mid-transaction leaves no torn state', () async {
final binding = (await fixture.store.resolveBinding())!;
final mutation = fixture.mutationFor(binding);
late Future<void> signOut;
final outcome = await fixture.store.write<DwRepoEnqueue>((tx) async {
if (!await tx.isBindingCurrent(binding)) return DwRepoEnqueue.stale;
signOut = fixture.signOut();
await tx.enqueue(mutation);
return DwRepoEnqueue.accepted;
});
await signOut;
final queued = (await fixture.queuedFor(
binding.scope,
)).map((queuedMutation) => queuedMutation.mutationId).toList();
expect(
queued,
outcome == DwRepoEnqueue.accepted ? [mutation.mutationId] : isEmpty,
reason:
'The answer and the queue disagree: write returned $outcome while '
'the queue holds $queued. The binding check has to read inside the '
'same transaction that commits the row — a check taken before '
'opening it is exactly this bug.',
);
});
});
}