verifyOAuthAuthorizationCodeExchangeStoreConformance function
Future<void>
verifyOAuthAuthorizationCodeExchangeStoreConformance(
- OAuthAuthorizationCodeExchangeStoreFactory createStore
Verifies the portable atomic exchange contract for a persistence adapter.
Durable adapters should run this function in their own test suite against an isolated database namespace. Passing these cases does not certify a backend transaction implementation; adapter authors must additionally fault the database commit itself and prove rollback.
Implementation
Future<void> verifyOAuthAuthorizationCodeExchangeStoreConformance(
OAuthAuthorizationCodeExchangeStoreFactory createStore,
) async {
await _case('exchange.commit', () async {
final fixture = await _fixture(await createStore(), 'commit');
final result = await fixture.store.commit(
request: fixture.request,
expectedAuthorizationId: fixture.code.authorizationId,
preparedToken: fixture.token,
);
_check(
result.status == OAuthAuthorizationCodeExchangeStatus.committed,
'exchange did not commit',
);
_check(
await fixture.store.accessTokenStore.findByToken(fixture.rawToken) !=
null,
'prepared token was not persisted',
);
});
await _case('exchange.concurrent-one-winner', () async {
final fixture = await _fixture(await createStore(), 'concurrent');
final results = await Future.wait(
List<Future<OAuthAuthorizationCodeExchangeResult>>.generate(
16,
(_) async => fixture.store.commit(
request: fixture.request,
expectedAuthorizationId: fixture.code.authorizationId,
preparedToken: fixture.token,
),
),
);
_check(
results
.where(
(result) =>
result.status ==
OAuthAuthorizationCodeExchangeStatus.committed,
)
.length ==
1,
'exchange had more than one winner',
);
_check(
results.every(
(result) =>
result.status == OAuthAuthorizationCodeExchangeStatus.committed ||
result.status ==
OAuthAuthorizationCodeExchangeStatus.alreadyCommitted,
),
'contending retry returned an unexpected result',
);
});
await _case('exchange.wrong-binding-consumes', () async {
final fixture = await _fixture(await createStore(), 'wrong-binding');
final wrong = OAuthAuthorizationCodeExchangeRequest(
codeHash: fixture.request.codeHash,
clientId: fixture.request.clientId,
redirectUri: fixture.request.redirectUri,
codeVerifier: 'wrong-verifier',
now: fixture.request.now,
);
_check(
(await fixture.store.prepare(wrong)).status ==
OAuthAuthorizationCodePreparationStatus.invalidGrant,
'wrong binding was accepted',
);
_check(
(await fixture.store.prepare(fixture.request)).status ==
OAuthAuthorizationCodePreparationStatus.invalidGrant,
'wrong binding did not consume the matching digest',
);
});
await _case('exchange.authorization-id-collision', () async {
final fixture = await _fixture(await createStore(), 'collision');
var rejected = false;
try {
await fixture.store.authorizationCodeStore.create(
_code(
namespace: 'collision-other',
authorizationId: fixture.code.authorizationId,
codeHash: hashOpaqueToken('collision-other-code'),
),
);
} on StateError {
rejected = true;
}
_check(rejected, 'duplicate authorization ID was accepted');
});
}