verifyAuthUsernameStoreConformance function

Future<void> verifyAuthUsernameStoreConformance(
  1. AuthUsernameStoreConformanceFixture fixture
)

Verifies username reservation, rollback, contention, replay, removal, and hard-deletion guarantees for a durable AuthStore adapter.

Implementation

Future<void> verifyAuthUsernameStoreConformance(
  AuthUsernameStoreConformanceFixture fixture,
) async {
  final root = fixture.store;
  if (root is! AuthUsernameStore) {
    throw const AuthUsernameStoreConformanceFailure(
      'capability',
      'Root AuthUsernameStore is required',
    );
  }
  final store = root as AuthUsernameStore;
  final authenticationMethods = AuthAuthenticationMethodService(
    store: root,
    contributors: <AuthAuthenticationMethodInventoryContributor>[
      _UsernameInventory(store, root),
      _OAuthInventory(root.accounts),
    ],
  )..composeContributors(const []);

  await _case('registration.concurrent-normalized-identifier', () async {
    final outcomes = await Future.wait([
      Future.sync(
        () => store.registerUsername(
          _registration('registration-a', 'shared-name'),
        ),
      ),
      Future.sync(
        () => store.registerUsername(
          _registration('registration-b', 'shared-name'),
        ),
      ),
    ]);
    _check(
      outcomes.where((result) => result.succeeded).length == 1,
      'registration winner',
    );
    _check(
      outcomes
              .where(
                (result) =>
                    result.status == AuthUsernameMutationStatus.conflict,
              )
              .length ==
          1,
      'registration conflict',
    );
  });

  await _case('registration.single-use-replay', () async {
    final command = _registration('registration-replay', 'replay-name');
    final first = await store.registerUsername(command);
    final replay = await store.registerUsername(command);
    _check(first.status == AuthUsernameMutationStatus.created, 'created');
    _check(
      replay.status == AuthUsernameMutationStatus.conflict,
      'replay conflict',
    );
    _check(
      (await store.findByUsername('replay-name'))?.id == first.credential?.id,
      'original ownership retained',
    );
  });

  await _case('registration.user-conflict-is-mutation-free', () async {
    final first = await store.registerUsername(
      _registration('registration-owner', 'owner-name'),
    );
    final now = DateTime.utc(2030);
    final conflictingUser = AuthUser(
      id: first.user!.id,
      email: first.user!.email,
      name: 'shadow-name',
      attributes: const <String, dynamic>{'username': 'shadow-name'},
    );
    final result = await store.registerUsername(
      AuthUsernameRegistrationCommand(
        user: conflictingUser,
        credential: AuthPasswordCredential(
          id: 'registration-shadow-credential',
          userId: conflictingUser.id,
          identifier: 'shadow-name',
          passwordHash: 'encoded-hash',
          createdAt: now,
          updatedAt: now,
        ),
      ),
    );
    _check(
      result.status == AuthUsernameMutationStatus.conflict,
      'user conflict',
    );
    _check(await store.findByUsername('shadow-name') == null, 'no credential');
    _check(
      (await store.findUsernameForUser(conflictingUser.id))?.identifier ==
          'owner-name',
      'original credential retained',
    );
    _check(
      (await root.users.findById(conflictingUser.id))?.attributes['username'] ==
          'owner-name',
      'original projection retained',
    );
  });

  await _case('change.conflict-rollback', () async {
    final first = await store.registerUsername(
      _registration('change-conflict-a', 'change-before'),
    );
    await store.registerUsername(
      _registration('change-conflict-b', 'change-claimed'),
    );
    final result = await store.changeUsername(
      _change(first, from: 'change-before', to: 'change-claimed'),
    );
    _check(result.status == AuthUsernameMutationStatus.conflict, 'conflict');
    _check(
      (await store.findUsernameForUser(first.user!.id))?.identifier ==
          'change-before',
      'credential rollback',
    );
    _check(
      (await root.users.findById(first.user!.id))?.attributes['username'] ==
          'change-before',
      'projection rollback',
    );
  });

  await _case('change.concurrent-and-replay', () async {
    final registered = await store.registerUsername(
      _registration('change-race', 'race-before'),
    );
    final commandA = _change(
      registered,
      from: 'race-before',
      to: 'race-after-a',
    );
    final commandB = _change(
      registered,
      from: 'race-before',
      to: 'race-after-b',
    );
    final outcomes = await Future.wait([
      Future.sync(() => store.changeUsername(commandA)),
      Future.sync(() => store.changeUsername(commandB)),
    ]);
    _check(
      outcomes
              .where(
                (result) => result.status == AuthUsernameMutationStatus.changed,
              )
              .length ==
          1,
      'rename winner',
    );
    final winner = outcomes.firstWhere((result) => result.succeeded);
    final replay = await store.changeUsername(
      _change(
        registered,
        from: 'race-before',
        to: winner.credential!.identifier,
      ),
    );
    _check(
      replay.status == AuthUsernameMutationStatus.unchanged,
      'idempotent replay',
    );
  });

  await _case('change.concurrent-target-reservation', () async {
    final first = await store.registerUsername(
      _registration('change-target-a', 'change-target-before-a'),
    );
    final second = await store.registerUsername(
      _registration('change-target-b', 'change-target-before-b'),
    );
    final outcomes = await Future.wait([
      Future.sync(
        () => store.changeUsername(
          _change(
            first,
            from: 'change-target-before-a',
            to: 'change-shared-target',
          ),
        ),
      ),
      Future.sync(
        () => store.changeUsername(
          _change(
            second,
            from: 'change-target-before-b',
            to: 'change-shared-target',
          ),
        ),
      ),
    ]);
    _check(
      outcomes
              .where(
                (result) => result.status == AuthUsernameMutationStatus.changed,
              )
              .length ==
          1,
      'shared target winner',
    );
    _check(
      outcomes
              .where(
                (result) =>
                    result.status == AuthUsernameMutationStatus.conflict,
              )
              .length ==
          1,
      'shared target conflict',
    );
    final winnerIndex = outcomes.indexWhere((result) => result.succeeded);
    final loser = winnerIndex == 0 ? second : first;
    _check(
      (await store.findByUsername('change-shared-target'))?.userId ==
          outcomes[winnerIndex].user?.id,
      'shared target ownership',
    );
    _check(
      (await store.findUsernameForUser(loser.user!.id))?.identifier ==
          (winnerIndex == 0
              ? 'change-target-before-b'
              : 'change-target-before-a'),
      'loser reservation preserved',
    );
  });

  await _case('availability.disabled-and-locked', () async {
    final disabled = await store.registerUsername(
      _registration('disabled', 'disabled-before'),
    );
    final disabledUser = disabled.user!;
    await root.users.update(
      AuthUser(
        id: disabledUser.id,
        email: disabledUser.email,
        name: disabledUser.name,
        attributes: <String, dynamic>{
          ...disabledUser.attributes,
          'disabled': true,
        },
      ),
    );
    final disabledResult = await store.changeUsername(
      _change(disabled, from: 'disabled-before', to: 'disabled-after'),
    );
    _check(
      disabledResult.status == AuthUsernameMutationStatus.userUnavailable,
      'disabled result',
    );
    if (root is AuthAccountStateStore) {
      final locked = await store.registerUsername(
        _registration('locked', 'locked-before'),
      );
      await (root as AuthAccountStateStore).upsert(
        AuthAccountState(
          userId: locked.user!.id,
          lockedUntil: DateTime.utc(2035),
        ),
      );
      final lockedResult = await store.changeUsername(
        _change(locked, from: 'locked-before', to: 'locked-after'),
      );
      _check(
        lockedResult.status == AuthUsernameMutationStatus.userUnavailable,
        'locked result',
      );
    }
  });

  final armFault = fixture.armFault;
  if (armFault != null) {
    await _case('fault.registration-rollback', () async {
      await armFault(AuthUsernameFaultPoint.registrationAfterUserWrite);
      final command = _registration('fault-register', 'fault-register-name');
      final error = await _capture(() => store.registerUsername(command));
      _check(error != null, 'fault surfaced');
      _check(
        await root.users.findById(command.user.id) == null,
        'user rollback',
      );
      _check(
        await store.findByUsername(command.credential.identifier) == null,
        'identifier rollback',
      );
      _check(
        (await store.registerUsername(command)).status ==
            AuthUsernameMutationStatus.created,
        'retry after rollback',
      );
    });

    await _case('fault.change-rollback', () async {
      final registered = await store.registerUsername(
        _registration('fault-change', 'fault-change-before'),
      );
      await armFault(AuthUsernameFaultPoint.changeAfterCredentialWrite);
      final error = await _capture(
        () => store.changeUsername(
          _change(
            registered,
            from: 'fault-change-before',
            to: 'fault-change-after',
          ),
        ),
      );
      _check(error != null, 'fault surfaced');
      _check(
        (await store.findUsernameForUser(registered.user!.id))?.identifier ==
            'fault-change-before',
        'credential rollback',
      );
      _check(
        (await root.users.findById(
              registered.user!.id,
            ))?.attributes['username'] ==
            'fault-change-before',
        'projection rollback',
      );
      _check(
        (await store.changeUsername(
              _change(
                registered,
                from: 'fault-change-before',
                to: 'fault-change-after',
              ),
            )).status ==
            AuthUsernameMutationStatus.changed,
        'retry after rollback',
      );
    });

    await _case('fault.removal-rollback', () async {
      final registered = await store.registerUsername(
        _registration('fault-remove', 'fault-remove-name'),
      );
      final userId = registered.user!.id;
      await root.accounts.link(
        AuthAccount(
          providerId: 'github',
          providerAccountId: 'fault-remove-account',
          userId: userId,
        ),
      );
      await armFault(AuthUsernameFaultPoint.removalAfterUserWrite);
      final error = await _capture(
        () => store.removeUsernameIfSafe(
          AuthUsernameRemovalCommand(
            userId: userId,
            credentialId: registered.credential!.id,
            loadInventory: () => authenticationMethods.snapshotForUser(userId),
          ),
        ),
      );
      _check(error != null, 'fault surfaced');
      _check(
        (await store.findUsernameForUser(userId))?.identifier ==
            'fault-remove-name',
        'credential rollback',
      );
      _check(
        (await root.users.findById(userId))?.attributes['username'] ==
            'fault-remove-name',
        'projection rollback',
      );
      _check(
        await store.removeUsernameIfSafe(
              AuthUsernameRemovalCommand(
                userId: userId,
                credentialId: registered.credential!.id,
                loadInventory: () =>
                    authenticationMethods.snapshotForUser(userId),
              ),
            ) ==
            AuthAuthenticationMethodMutationResult.mutated,
        'retry after rollback',
      );
    });
  }

  await _case('removal.inventory-and-replay', () async {
    final registered = await store.registerUsername(
      _registration('remove', 'remove-name'),
    );
    final userId = registered.user!.id;
    await root.accounts.link(
      AuthAccount(
        providerId: 'github',
        providerAccountId: 'remove-account',
        userId: userId,
      ),
    );
    final first = await store.removeUsernameIfSafe(
      AuthUsernameRemovalCommand(
        userId: userId,
        credentialId: registered.credential!.id,
        loadInventory: () => authenticationMethods.snapshotForUser(userId),
      ),
    );
    _check(first == AuthAuthenticationMethodMutationResult.mutated, 'removed');
    final replay = await store.removeUsernameIfSafe(
      AuthUsernameRemovalCommand(
        userId: userId,
        credentialId: registered.credential!.id,
        loadInventory: () => authenticationMethods.snapshotForUser(userId),
      ),
    );
    _check(replay == AuthAuthenticationMethodMutationResult.notFound, 'replay');
    _check(
      !(await root.users.findById(userId))!.attributes.containsKey('username'),
      'projection removal',
    );
  });

  if (root is AuthAdminStoreCapabilities &&
      root is AuthUserDeletionCoordinatorHost) {
    await _case('hard-deletion.releases-identifier', () async {
      (root as AuthUserDeletionCoordinatorHost)
          .bindUserDeletionPlanContributors(const []);
      final registered = await store.registerUsername(
        _registration('hard-delete', 'hard-delete-name'),
      );
      _check(
        await (root as AuthAdminStoreCapabilities).deleteUserForAdministration(
          registered.user!.id,
        ),
        'hard deletion',
      );
      _check(await store.findByUsername('hard-delete-name') == null, 'cleanup');
      final reused = await store.registerUsername(
        _registration('hard-delete-reuse', 'hard-delete-name'),
      );
      _check(reused.succeeded, 'identifier reuse');
    });
  }
}