feat(base)!: removing a profile makes it private (ADR 0020, D27)
Some checks failed
CI / check (push) Failing after 30m48s
CI / deploy (push) Has been skipped

Every account keeps a handle now, so removing a public profile no
longer deletes it and frees the handle: it takes away what the profile
shows and leaves it private, as a profile is until its person goes
public (D5).

- `profile.remove` and the register's `removeProfile` (members ask the
  register) empty the display name, bio, links with their checks and
  the avatar, keep the row with its handle, and return what is left,
  null for none. `/@handle` and `profiles.byHandle` then answer as for
  a handle nobody has. Once the person has published, removing is
  refused as before.
- Learn: the profile form starts afresh from what is left, "Remove
  profile" shows only while something is filled in, and the dialog says
  the handle stays.
- The module's README and ADR 0020 amended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-24 18:26:56 +02:00
commit d5651c5beb
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 255 additions and 60 deletions

View file

@ -214,13 +214,58 @@ void main() {
expect((renamed.handle, renamed.handleFixed), ('Alice_JP', true));
});
test('removing a profile frees its handle', () async {
await save(alice, 'alice');
test('removing a profile makes it private: its handle stays its '
"person's, what was filled in goes, and others find it as a handle "
'nobody has (ADR 0020, D27)', () async {
await save(
alice,
'Alice',
displayName: 'Alice',
bio: 'Learning Japanese.',
links: ['https://halkyon.app'],
);
expect(
(await endpoints.profiles.byHandle(bob, 'alice')).handle,
'Alice',
);
await endpoints.profile.remove(alice);
final removed = (await endpoints.profile.remove(alice))!;
expect(await endpoints.profile.mine(alice), isNull);
expect((await save(bob, 'alice')).handle, 'alice');
expect(
(
removed.handle,
removed.displayName,
removed.bio,
removed.avatarUrl,
removed.handleFixed,
),
('Alice', '', '', null, false),
);
expect(removed.links, isEmpty);
expect(removed.verifiedLinks ?? const <String>[], isEmpty);
expect(
(await endpoints.profile.mine(alice))!.toJson(),
removed.toJson(),
);
await expectLater(
endpoints.profiles.byHandle(bob, 'alice'),
throwsA(isA<ProfileNotFoundException>()),
);
await expectLater(
save(bob, 'alice'),
refused(ProfileRefusal.handleTaken),
);
// Filled in again, it is public again.
await save(alice, 'Alice', displayName: 'Alice');
expect(
(await endpoints.profiles.byHandle(bob, 'alice')).displayName,
'Alice',
);
});
test('removing nothing gives nothing: without a profile', () async {
expect(await endpoints.profile.remove(bob), isNull);
expect(await endpoints.profile.mine(bob), isNull);
});
test('a profile is found by its handle only once its person goes '

View file

@ -181,8 +181,23 @@ final class _Register implements AccountRegister {
_answer('removeAvatar', () => profiles[person]!);
@override
Future<void> removeProfile(Session session, UuidValue person) =>
_answer('removeProfile', () => profiles.remove(person));
Future<Profile?> removeProfile(Session session, UuidValue person) =>
_answer('removeProfile', () {
final profile = profiles[person];
if (profile == null) {
return null;
}
// Private again, the handle kept (ADR 0020, D27).
return profiles[person] = Profile(
handle: profile.handle,
address: profile.address,
displayName: '',
bio: '',
links: const [],
pageUrl: profile.pageUrl,
handleFixed: profile.handleFixed,
);
});
@override
Future<void> fixHandle(Session session, UuidValue person) =>
@ -1710,6 +1725,17 @@ void main() {
expect(await PublicProfile.db.count(session), 0);
});
test('are removed at the register, which says what is left: the '
'handle (ADR 0020, D27)', () async {
await endpoints.profile.save(asBen(), 'Ben', 'Ben', '', const []);
final removed = (await endpoints.profile.remove(asBen()))!;
expect((removed.handle, removed.displayName), ('Ben', ''));
expect(register.asked.last, 'removeProfile');
expect(register.profiles[ben]!.toJson(), removed.toJson());
});
test('have their handles checked at the register, the device '
'network passed on to be counted there', () async {
register.handleRefusal = ProfileRefusal.tooManyHandleChecks;

View file

@ -318,6 +318,22 @@ void main() {
refused(ProfileRefusal.handleTaken),
);
await remote.saveProfile(
session,
anna,
handle: 'Anna',
displayName: 'Anna',
bio: '',
links: const [],
);
final removed = (await remote.removeProfile(session, anna))!;
expect(
(removed.handle, removed.displayName),
('Anna', ''),
reason: 'removing keeps the handle, private (ADR 0020, D27)',
);
expect(await remote.removeProfile(session, ben), isNull);
await remote.fixHandle(session, anna);
expect((await remote.profileOf(session, anna))!.handleFixed, isTrue);

View file

@ -1720,7 +1720,9 @@ class _ProfileEndpoint {
});
}
_ida.Future<void> remove(_ist.TestSessionBuilder sessionBuilder) async {
_ida.Future<_it4tom37.Profile?> remove(
_ist.TestSessionBuilder sessionBuilder,
) async {
return _ist.callAwaitableFunctionAndHandleExceptions(() async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild(
@ -1740,7 +1742,7 @@ class _ProfileEndpoint {
_localUniqueSession,
_localCallContext.arguments,
)
as _ida.Future<void>);
as _ida.Future<_it4tom37.Profile?>);
return _localReturnValue;
} finally {
await _localUniqueSession.close();