check static method
- required WincheService create(
- WincheApp app
- Object? sessionHandle(
- WincheService service
- WincheOptions? options,
Runs every check against a service built by create.
sessionHandle returns an opaque value standing for the service's
current internal session — its store handle, its connection, or a build
counter — and null when it holds none. It must be a different value after
a user switch and the same value after a token rotation.
Do not return core's WincheSession here. That object belongs to core, so it stays identical through an internal rebuild, and the rotation check could never fail — the suite would certify a service that tears down and reopens its store on every token refresh. Return something only your implementation could produce.
Without it the binding checks are skipped, and only sequencing, failure and teardown behaviour are verified.
options is passed to the app under test, for a service that needs an
endpoint or directory root to build a session at all.
Implementation
static Future<WincheContractReport> check({
required WincheService Function(WincheApp app) create,
Object? Function(WincheService service)? sessionHandle,
WincheOptions? options,
}) async {
final recorder = ContractRecorder();
Future<void> withHarness(Future<void> Function(_Harness harness) body) async {
final harness = _Harness(create: create, options: options);
try {
await body(harness);
} finally {
await harness.close();
}
}
const bindingChecks = [
'binds when an identity arrives',
'unbinds on sign-out',
'swaps its session on a user switch',
'does not rebuild on a token rotation',
];
if (sessionHandle == null) {
for (final check in bindingChecks) {
recorder.skip(check, 'no sessionHandle driver was provided');
}
} else {
await recorder.expect(bindingChecks[0], () async {
await withHarness((h) async {
recorder.require(sessionHandle(h.service) == null, 'held a session before any identity was announced');
await h.signIn('alice');
recorder.require(
sessionHandle(h.service) != null,
'built nothing after an identity was announced — did onSessionChanged do anything?',
);
});
});
await recorder.expect(bindingChecks[1], () async {
await withHarness((h) async {
await h.signIn('alice');
await h.signOut();
recorder.require(
sessionHandle(h.service) == null,
'still held a session after sign-out; the previous user\'s store or connection is still open',
);
});
});
await recorder.expect(bindingChecks[2], () async {
await withHarness((h) async {
await h.signIn('alice');
final alice = sessionHandle(h.service);
await h.signIn('bob');
final bob = sessionHandle(h.service);
recorder.require(alice != null && bob != null, 'held nothing for one of the two users');
recorder.require(
alice != bob,
'kept the same internal session across a user switch — bob would be reading alice\'s state',
);
});
});
await recorder.expect(bindingChecks[3], () async {
await withHarness((h) async {
await h.signIn('alice');
final before = sessionHandle(h.service);
await h.rotateToken();
recorder.require(
sessionHandle(h.service) == before,
'rebuilt on a token rotation. A rotation must nudge: tearing down and reopening a store '
'and socket on every token refresh is exactly what onTokenChanged exists to avoid',
);
});
});
}
await recorder.expect('completes its hooks within the deadline', () async {
await withHarness((h) async {
await h.signIn('alice');
await h.signOut();
recorder.require(
h.errors.isEmpty,
'reported ${h.errors.length} failure(s) during a plain sign-in/sign-out: '
'${h.errors.join(', ')}',
);
});
});
await recorder.expect('survives a sign-out it never bound for', () async {
await withHarness((h) async {
await h.signOut();
recorder.require(h.errors.isEmpty, 'threw on a sign-out while already unbound');
});
});
await recorder.expect('dispose is idempotent and deregisters', () async {
await withHarness((h) async {
await h.signIn('alice');
await h.service.dispose();
recorder.require(h.service.isDisposed, 'isDisposed was false after dispose()');
await h.service.dispose();
});
});
return recorder.report;
}