feat(base): handles hidden until their people go public, every answer about a new handle counted (ADR 0020, D5)
A handle's page and profiles.byHandle answer as for a handle nobody has until its person goes public: a handle fixed by a publish, or a display name, a bio, a link or an avatar (isPublic). A person's own profile, and the host's own lookups (personWithHandle), are unchanged. So every answer about a new handle now tells that an account has it, and countHandleCheck counts them together on the register, 30 an hour per person (or attempt) and 60 per network, refused beyond with ProfileRefusal.tooManyHandleChecks: profile.checkHandle and saving a profile with a handle new for the person, simpleLogin.checkHandle and signUp, keys.set with a handle, and Halkyon Learn's inbox.block and unblock, which look people up by handle. A person's own handle and what is no handle aren't counted. The SimpleLogin limiters of commit 3 give way to it, so a network's 60 are shared between people signed in and people new. The register's checkHandle, saveProfile, signUpWithSimpleLogin and setFirstKeys carry the visitor's network from the member that asked. Halkyon Learn's handle dialog words the new refusal (English, German). Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e5b9578916
commit
7eb1961b9f
6 changed files with 674 additions and 134 deletions
|
|
@ -460,6 +460,23 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
test("count a new handle among the person's answers about handles "
|
||||
'(30 an hour), the sign-in still standing when refused', () async {
|
||||
final attempt = await signInAgain(onBensLaptop(), 'sl-ben');
|
||||
for (var i = 0; i < handleChecksPerAsker; i++) {
|
||||
await countHandleCheck(session, asker: ben.uuid, network: null);
|
||||
}
|
||||
|
||||
await expectLater(
|
||||
setWith(attempt),
|
||||
refusedAsProfile(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
expect(
|
||||
await idp().isKept(session, attempt: attempt, caller: ben),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('refuse an account with keys already, under either kind of '
|
||||
'password', () async {
|
||||
final annas = await signInAgain(onPhone(), 'sl-anna');
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ void main() {
|
|||
useBase(sessionBuilder);
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
tearDown(
|
||||
() => PublicProfile.db.deleteWhere(session, where: (t) => t.id > 0),
|
||||
);
|
||||
tearDown(() async {
|
||||
await PublicProfile.db.deleteWhere(session, where: (t) => t.id > 0);
|
||||
await RateLimitedRequestAttempt.db.deleteWhere(
|
||||
session,
|
||||
where: (t) => t.id.notEquals(null),
|
||||
);
|
||||
});
|
||||
|
||||
/// A person with an identity, signed in.
|
||||
TestSessionBuilder identifiedPerson() => sessionBuilder.copyWith(
|
||||
|
|
@ -219,6 +223,100 @@ void main() {
|
|||
expect((await save(bob, 'alice')).handle, 'alice');
|
||||
});
|
||||
|
||||
test('a profile is found by its handle only once its person goes '
|
||||
'public: with something published, or the profile filled in; '
|
||||
'their own they see all along (D5)', () async {
|
||||
Future<String?> found() async {
|
||||
try {
|
||||
return (await endpoints.profiles.byHandle(bob, 'alice')).handle;
|
||||
} on ProfileNotFoundException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
await save(alice, 'Alice');
|
||||
expect(await found(), isNull);
|
||||
expect((await endpoints.profile.mine(alice))!.handle, 'Alice');
|
||||
|
||||
for (final (displayName, bio, links) in [
|
||||
('Alice', '', const <String>[]),
|
||||
('', 'Learning Japanese.', const <String>[]),
|
||||
('', '', ['https://halkyon.app']),
|
||||
]) {
|
||||
await save(
|
||||
alice,
|
||||
'Alice',
|
||||
displayName: displayName,
|
||||
bio: bio,
|
||||
links: links,
|
||||
);
|
||||
expect(await found(), 'Alice', reason: '$displayName$bio$links');
|
||||
}
|
||||
await save(alice, 'Alice');
|
||||
expect(await found(), isNull);
|
||||
|
||||
final row = (await PublicProfile.db.findFirstRow(
|
||||
session,
|
||||
where: (t) => t.handleKey.equals('alice'),
|
||||
))!;
|
||||
await PublicProfile.db.updateRow(
|
||||
session,
|
||||
row.copyWith(firstPublishedAt: clock.now().toUtc()),
|
||||
);
|
||||
expect(await found(), 'Alice');
|
||||
});
|
||||
|
||||
group('Answers about handles', () {
|
||||
Future<ProfileRefusal?> check(
|
||||
TestSessionBuilder person,
|
||||
String handle,
|
||||
) => endpoints.profile.checkHandle(person, handle);
|
||||
|
||||
test('are at most 30 an hour for a person: checking a handle and '
|
||||
'saving a new one count, a refused one neither', () async {
|
||||
await save(alice, 'alice');
|
||||
for (var i = 0; i < 29; i++) {
|
||||
await check(bob, 'bob$i');
|
||||
}
|
||||
await save(bob, 'bob');
|
||||
|
||||
await expectLater(
|
||||
check(bob, 'alice'),
|
||||
refused(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
await expectLater(
|
||||
save(bob, 'alice'),
|
||||
refused(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
expect((await endpoints.profile.mine(bob))!.handle, 'bob');
|
||||
// Saving their own handle again, whatever its case, answers nothing
|
||||
// new, nor does checking it, or a handle that is none.
|
||||
expect(
|
||||
(await save(bob, 'Bob', displayName: 'Bob')).displayName,
|
||||
'Bob',
|
||||
);
|
||||
expect(await check(bob, 'BOB'), isNull);
|
||||
expect(await check(bob, 'a-b'), ProfileRefusal.handleInvalid);
|
||||
expect(await check(bob, 'admin'), ProfileRefusal.handleReserved);
|
||||
// Others ask on.
|
||||
expect(await check(alice, 'carla'), isNull);
|
||||
});
|
||||
|
||||
test('are at most 60 an hour from one network, whoever asks', () async {
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await check(alice, 'carla$i');
|
||||
}
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await check(bob, 'dora$i');
|
||||
}
|
||||
|
||||
await expectLater(
|
||||
check(identifiedPerson(), 'emil'),
|
||||
refused(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("a handle nobody has isn't found", () async {
|
||||
await expectLater(
|
||||
endpoints.profiles.byHandle(alice, 'nobody'),
|
||||
|
|
@ -306,6 +404,22 @@ void main() {
|
|||
expect(await stored(), 0);
|
||||
});
|
||||
|
||||
test('makes a profile public by itself (D5)', () async {
|
||||
await save(alice, 'alice');
|
||||
|
||||
await endpoints.profile.setAvatar(alice, png(1));
|
||||
expect(
|
||||
(await endpoints.profiles.byHandle(bob, 'alice')).handle,
|
||||
'alice',
|
||||
);
|
||||
|
||||
await endpoints.profile.removeAvatar(alice);
|
||||
await expectLater(
|
||||
endpoints.profiles.byHandle(bob, 'alice'),
|
||||
throwsA(isA<ProfileNotFoundException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('needs a profile to show on', () async {
|
||||
await expectLater(
|
||||
endpoints.profile.setAvatar(alice, png(1)),
|
||||
|
|
|
|||
|
|
@ -222,6 +222,23 @@ void main() {
|
|||
expect(page.html, isNot(contains('class="app"')));
|
||||
});
|
||||
|
||||
test('is not found until its person goes public (D5)', () async {
|
||||
useBase(sessionBuilder);
|
||||
await endpoints.profile.save(alice, 'alice', '', '', const []);
|
||||
final host = profilePageUrl(session.serverpod, 'alice').host;
|
||||
|
||||
expect(
|
||||
(await profilePage(session, '@alice', host: host)).status,
|
||||
404,
|
||||
);
|
||||
|
||||
await save(alice, 'alice', const []);
|
||||
expect(
|
||||
(await profilePage(session, '@alice', host: host)).status,
|
||||
200,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'is not found for a handle nobody has, or anything else',
|
||||
() async {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ final class _Register implements AccountRegister {
|
|||
final simpleLoginLinked = <UuidValue>{};
|
||||
SimpleLoginProof? simpleLoginProof;
|
||||
|
||||
/// The handles checked for someone new and what each check says, the
|
||||
/// sign-ups of people new with SimpleLogin, and the proofs keys were
|
||||
/// unlocked with, as members bound them.
|
||||
/// The handles checked (for someone new, or signed in) and what each
|
||||
/// check says, the sign-ups of people new with SimpleLogin, and the proofs
|
||||
/// keys were unlocked with, as members bound them.
|
||||
final handlesChecked = <String>[];
|
||||
ProfileRefusal? handleRefusal;
|
||||
final simpleLoginSignUps = <({UuidValue attempt, String handle})>[];
|
||||
|
|
@ -145,9 +145,10 @@ final class _Register implements AccountRegister {
|
|||
required String displayName,
|
||||
required String bio,
|
||||
required List<String> links,
|
||||
}) => _answer(
|
||||
'saveProfile',
|
||||
() => profiles[person] = Profile(
|
||||
String? network,
|
||||
}) => _answer('saveProfile', () {
|
||||
networks.add(network!);
|
||||
return profiles[person] = Profile(
|
||||
handle: handle,
|
||||
address: '$handle@learn.test',
|
||||
displayName: displayName,
|
||||
|
|
@ -155,15 +156,20 @@ final class _Register implements AccountRegister {
|
|||
links: links,
|
||||
pageUrl: 'https://learn.test/@$handle',
|
||||
handleFixed: false,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
Future<ProfileRefusal?> checkHandle(
|
||||
Session session,
|
||||
UuidValue person,
|
||||
String handle,
|
||||
) => _answer('checkHandle', () => null);
|
||||
String handle, {
|
||||
required String network,
|
||||
}) => _answer('checkHandle', () {
|
||||
networks.add(network);
|
||||
handlesChecked.add(handle);
|
||||
return handleRefusal;
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Profile> checkLinks(Session session, UuidValue person) =>
|
||||
|
|
@ -314,7 +320,9 @@ final class _Register implements AccountRegister {
|
|||
required UuidValue attempt,
|
||||
required String handle,
|
||||
required KeysSetup setup,
|
||||
required String network,
|
||||
}) => _answer('signUpWithSimpleLogin', () {
|
||||
networks.add(network);
|
||||
final account = const Uuid().v7obj();
|
||||
add(account);
|
||||
simpleLoginSignUps.add((attempt: attempt, handle: handle));
|
||||
|
|
@ -334,8 +342,10 @@ final class _Register implements AccountRegister {
|
|||
required UuidValue person,
|
||||
required UuidValue attempt,
|
||||
required KeysSetup setup,
|
||||
required String network,
|
||||
String? handle,
|
||||
}) => _answer('setFirstKeys', () {
|
||||
networks.add(network);
|
||||
firstKeys[person] = (attempt: attempt, handle: handle);
|
||||
});
|
||||
|
||||
|
|
@ -1333,6 +1343,7 @@ void main() {
|
|||
|
||||
final carla = signedUp.authUserId;
|
||||
expect(register.asked, ['signUpWithSimpleLogin']);
|
||||
expect(register.networks, hasLength(1));
|
||||
expect(register.simpleLoginSignUps.single, (
|
||||
attempt: attempt,
|
||||
handle: 'carla',
|
||||
|
|
@ -1532,6 +1543,7 @@ void main() {
|
|||
);
|
||||
|
||||
expect(register.asked, ['setFirstKeys']);
|
||||
expect(register.networks, hasLength(1));
|
||||
expect(register.firstKeys, {ben: (attempt: attempt, handle: 'ben')});
|
||||
});
|
||||
|
||||
|
|
@ -1651,6 +1663,7 @@ void main() {
|
|||
);
|
||||
|
||||
expect(register.profiles[ben]!.toJson(), saved.toJson());
|
||||
expect(register.networks, hasLength(1));
|
||||
expect(
|
||||
(await endpoints.profiles.byHandle(asAnna(), 'ben')).toJson(),
|
||||
saved.toJson(),
|
||||
|
|
@ -1659,6 +1672,19 @@ void main() {
|
|||
expect(await PublicProfile.db.count(session), 0);
|
||||
});
|
||||
|
||||
test('have their handles checked at the register, the device '
|
||||
'network passed on to be counted there', () async {
|
||||
register.handleRefusal = ProfileRefusal.tooManyHandleChecks;
|
||||
|
||||
expect(
|
||||
await endpoints.profile.checkHandle(asBen(), 'Ben'),
|
||||
ProfileRefusal.tooManyHandleChecks,
|
||||
);
|
||||
expect(register.asked, ['checkHandle']);
|
||||
expect(register.handlesChecked, ['Ben']);
|
||||
expect(register.networks, hasLength(1));
|
||||
});
|
||||
|
||||
test("have their page at the register's address, where this "
|
||||
"server's sends its visitors", () async {
|
||||
await endpoints.profile.save(asBen(), 'Ben', 'Ben', '', const []);
|
||||
|
|
|
|||
|
|
@ -288,9 +288,22 @@ void main() {
|
|||
);
|
||||
expect(await remote.personWithHandle(session, 'ANNA'), anna);
|
||||
expect((await remote.profilesOf(session, {anna, ben})).keys, [anna]);
|
||||
expect(await remote.checkHandle(session, anna, 'Anna'), isNull);
|
||||
expect(
|
||||
await remote.checkHandle(session, ben, 'anna'),
|
||||
await remote.checkHandle(
|
||||
session,
|
||||
anna,
|
||||
'Anna',
|
||||
network: '203.0.113.7',
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await remote.checkHandle(
|
||||
session,
|
||||
ben,
|
||||
'anna',
|
||||
network: '203.0.113.7',
|
||||
),
|
||||
ProfileRefusal.handleTaken,
|
||||
);
|
||||
await expectLater(
|
||||
|
|
@ -314,6 +327,57 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test("not public yet are nobody's to a member's apps, while its code "
|
||||
'still finds whose a handle is (D5)', () async {
|
||||
final anna = const Uuid().v7obj();
|
||||
await remote.saveProfile(
|
||||
session,
|
||||
anna,
|
||||
handle: 'Anna',
|
||||
displayName: '',
|
||||
bio: '',
|
||||
links: const [],
|
||||
network: '198.51.100.7',
|
||||
);
|
||||
|
||||
expect(await remote.profileByHandle(session, 'anna'), isNull);
|
||||
expect(await remote.personWithHandle(session, 'anna'), anna);
|
||||
expect((await remote.profileOf(session, anna))!.handle, 'Anna');
|
||||
});
|
||||
|
||||
test("count a member's answers about handles on the register, by "
|
||||
"the person and the visitor's network", () async {
|
||||
final anna = const Uuid().v7obj();
|
||||
await remote.saveProfile(
|
||||
session,
|
||||
anna,
|
||||
handle: 'Anna',
|
||||
displayName: '',
|
||||
bio: '',
|
||||
links: const [],
|
||||
network: '198.51.100.7',
|
||||
);
|
||||
await remote.checkHandle(
|
||||
session,
|
||||
anna,
|
||||
'Anna_G',
|
||||
network: '198.51.100.7',
|
||||
);
|
||||
|
||||
final counted = await RateLimitedRequestAttempt.db.find(
|
||||
session,
|
||||
where: (t) => t.domain.equals('profile'),
|
||||
);
|
||||
expect(
|
||||
counted.map((note) => (note.source, note.key)).toSet(),
|
||||
{
|
||||
('handle_checks', anna.uuid),
|
||||
('handle_checks_per_network', '198.51.100.7'),
|
||||
},
|
||||
);
|
||||
expect(counted, hasLength(4));
|
||||
});
|
||||
|
||||
test('missing are said to be so', () async {
|
||||
final nobody = const Uuid().v7obj();
|
||||
|
||||
|
|
@ -639,6 +703,7 @@ void main() {
|
|||
attempt: start.attempt,
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
|
||||
expect(
|
||||
|
|
@ -699,6 +764,7 @@ void main() {
|
|||
attempt: start.attempt,
|
||||
handle: 'admin',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
),
|
||||
refused(ProfileRefusal.handleReserved),
|
||||
);
|
||||
|
|
@ -708,6 +774,7 @@ void main() {
|
|||
attempt: const Uuid().v7obj(),
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
),
|
||||
refused(OidcRefusal.unknownAttempt),
|
||||
);
|
||||
|
|
@ -720,6 +787,7 @@ void main() {
|
|||
attempt: start.attempt,
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
await expectLater(
|
||||
remote.unlockKeys(
|
||||
|
|
@ -771,6 +839,7 @@ void main() {
|
|||
person: ben,
|
||||
attempt: attempt,
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
),
|
||||
refused(ProfileRefusal.handleInvalid),
|
||||
);
|
||||
|
|
@ -780,6 +849,7 @@ void main() {
|
|||
person: ben,
|
||||
attempt: const Uuid().v7obj(),
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
handle: 'ben',
|
||||
),
|
||||
refused(OidcRefusal.unknownAttempt),
|
||||
|
|
@ -789,6 +859,7 @@ void main() {
|
|||
person: ben,
|
||||
attempt: attempt,
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
handle: 'ben',
|
||||
);
|
||||
|
||||
|
|
@ -800,6 +871,7 @@ void main() {
|
|||
person: ben,
|
||||
attempt: await signedInAgain(ben, 'sl-ben'),
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
),
|
||||
refused(PasswordRefusal.passwordExists),
|
||||
);
|
||||
|
|
@ -829,6 +901,7 @@ void main() {
|
|||
attempt: start.attempt,
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
final keepSession = const Uuid().v7obj();
|
||||
|
||||
|
|
@ -931,6 +1004,7 @@ void main() {
|
|||
attempt: first.attempt,
|
||||
handle: 'Dora',
|
||||
setup: device.setup(),
|
||||
network: '203.0.113.0/24',
|
||||
);
|
||||
final start = await remote.beginSimpleLogin(
|
||||
session,
|
||||
|
|
|
|||
|
|
@ -952,6 +952,56 @@ void main() {
|
|||
refusedAsProfile(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
});
|
||||
|
||||
test('counts signing up with a handle, which says as much; the '
|
||||
'identity left waiting when refused', () async {
|
||||
final attempt = await newcomer();
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await endpoints.simpleLogin.checkHandle(
|
||||
notSignedIn(),
|
||||
attempt: attempt,
|
||||
handle: 'carla$i',
|
||||
);
|
||||
}
|
||||
|
||||
await expectLater(
|
||||
endpoints.simpleLogin.signUp(
|
||||
notSignedIn(),
|
||||
attempt: attempt,
|
||||
handle: 'carla',
|
||||
setup: device.setup(),
|
||||
),
|
||||
refusedAsProfile(ProfileRefusal.tooManyHandleChecks),
|
||||
);
|
||||
expect(
|
||||
await AuthServices.instance.oidcIdp.isKept(
|
||||
session,
|
||||
attempt: attempt,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test("doesn't count a handle that is none, which says nothing of "
|
||||
'anyone', () async {
|
||||
final attempt = await newcomer();
|
||||
for (var i = 0; i < 30; i++) {
|
||||
await endpoints.simpleLogin.checkHandle(
|
||||
notSignedIn(),
|
||||
attempt: attempt,
|
||||
handle: 'carla$i',
|
||||
);
|
||||
}
|
||||
|
||||
expect(
|
||||
await endpoints.simpleLogin.checkHandle(
|
||||
notSignedIn(),
|
||||
attempt: attempt,
|
||||
handle: 'carla!',
|
||||
),
|
||||
ProfileRefusal.handleInvalid,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue