feat(app,learn): shared spaces' members on the device
Step 4 (e) of ADR 0044, its first part, for every app's screens. - halkyon_app: `SharedSpaceGateway` on the module's `sharedSpace` endpoint (the spaces, their members and roles, inviting, handing on, leaving), refusals as `SpaceRefused`; `SharedKeys.trustIn` gives each member's safety code and whether it was compared or their keys changed, `markCompared`, and `trustNewKeys`, which pins the keys whose code was compared and never those the instance hands next. `FakeSharedSpaceServer` keeps the module's rules for tests, in step with `FakeSharedKeysServer`, which now knows members' handles. - Learn's sharing makes its space through it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4b900aa930
commit
02bb24428c
2 changed files with 688 additions and 10 deletions
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
||||
/// The fake instance's shared spaces keep halkyon_base's rules (ADR 0044,
|
||||
/// D2, D11), which screens are tested against: the Owner alone manages the
|
||||
/// members, the Owner's role passes only by handing on, and the keys'
|
||||
/// members follow.
|
||||
void main() {
|
||||
late FakeSharedKeysServer keys;
|
||||
late FakeSharedSpaceServer server;
|
||||
late SharedSpaceGateway alice;
|
||||
late SharedSpaceGateway bob;
|
||||
late String space;
|
||||
|
||||
Matcher refused(SpaceRefusal reason) => throwsA(
|
||||
isA<SpaceRefused>().having((r) => r.reason, 'reason', reason),
|
||||
);
|
||||
|
||||
setUp(() async {
|
||||
keys = FakeSharedKeysServer();
|
||||
server = FakeSharedSpaceServer(keys)
|
||||
..addPerson('alice', handle: 'alice')
|
||||
..addPerson('bob', handle: 'Bob')
|
||||
..addPerson('carol', handle: 'carol');
|
||||
alice = server.as('alice');
|
||||
bob = server.as('bob');
|
||||
space = await alice.create('studyset');
|
||||
});
|
||||
|
||||
test('the Owner invites by handle, whatever its case; the invited are '
|
||||
'members with the role given', () async {
|
||||
final invited = await alice.invite(space, 'bob', SpaceRole.viewer);
|
||||
expect(invited.handle, 'Bob');
|
||||
expect(invited.role, SpaceRole.viewer);
|
||||
expect(await bob.mine(), [
|
||||
(id: space, kind: 'studyset', role: SpaceRole.viewer),
|
||||
]);
|
||||
expect(keys.as('bob').keysNeeded(space), completion(isEmpty));
|
||||
|
||||
await expectLater(
|
||||
alice.invite(space, 'nobody', SpaceRole.editor),
|
||||
refused(SpaceRefusal.noSuchHandle),
|
||||
);
|
||||
await expectLater(
|
||||
alice.invite(space, 'BOB', SpaceRole.editor),
|
||||
refused(SpaceRefusal.alreadyMember),
|
||||
);
|
||||
await expectLater(
|
||||
alice.invite(space, 'carol', SpaceRole.owner),
|
||||
refused(SpaceRefusal.ownerRole),
|
||||
);
|
||||
});
|
||||
|
||||
test('only the Owner manages the members', () async {
|
||||
await alice.invite(space, 'bob', SpaceRole.editor);
|
||||
final carol = await alice.invite(space, 'carol', SpaceRole.viewer);
|
||||
|
||||
await expectLater(
|
||||
bob.invite(space, 'carol', SpaceRole.viewer),
|
||||
refused(SpaceRefusal.notAllowed),
|
||||
);
|
||||
await expectLater(
|
||||
bob.setRole(space, carol.id, SpaceRole.editor),
|
||||
refused(SpaceRefusal.notAllowed),
|
||||
);
|
||||
await expectLater(
|
||||
bob.remove(space, carol.id),
|
||||
refused(SpaceRefusal.notAllowed),
|
||||
);
|
||||
await expectLater(bob.delete(space), refused(SpaceRefusal.notAllowed));
|
||||
await expectLater(
|
||||
server.as('mallory').members(space),
|
||||
refused(SpaceRefusal.notFound),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'the Owner hands on before leaving; the last member deletes instead',
|
||||
() async {
|
||||
final mine = (await alice.members(space)).single;
|
||||
await expectLater(alice.leave(space), refused(SpaceRefusal.lastMember));
|
||||
await expectLater(
|
||||
alice.remove(space, mine.id),
|
||||
refused(SpaceRefusal.self),
|
||||
);
|
||||
|
||||
final invited = await alice.invite(space, 'bob', SpaceRole.viewer);
|
||||
await expectLater(
|
||||
alice.leave(space),
|
||||
refused(SpaceRefusal.nextOwnerNeeded),
|
||||
);
|
||||
await alice.leave(space, nextOwner: invited.id);
|
||||
|
||||
expect(await alice.mine(), isEmpty);
|
||||
expect((await bob.mine()).single.role, SpaceRole.owner);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -219,4 +219,43 @@ void main() {
|
|||
|
||||
expect(await bob.openMine(space), openedThese({}, refused: {'k1'}));
|
||||
});
|
||||
|
||||
test("a member's safety code is the same on both sides, and is marked "
|
||||
'compared once the person says so (D32)', () async {
|
||||
final seen = (await alice.trustIn(space)).single;
|
||||
expect(seen.person, 'bob');
|
||||
expect(seen.changed, isFalse);
|
||||
expect(seen.compared, isFalse);
|
||||
expect(seen.code, (await bob.trustIn(space)).single.code);
|
||||
|
||||
await alice.markCompared('bob');
|
||||
expect((await alice.trustIn(space)).single.compared, isTrue);
|
||||
});
|
||||
|
||||
test('keys that changed are flagged with the code of the new ones, and '
|
||||
'only the keys whose code was compared are trusted anew', () async {
|
||||
await alice.trustIn(space);
|
||||
await alice.markCompared('bob');
|
||||
final mallorys = keysOf(malloryAccount);
|
||||
final eves = keysOf((await crypto.newAccount('eve password')).keys);
|
||||
server.handOutKeys('bob', mallorys);
|
||||
|
||||
final changed = (await alice.trustIn(space)).single;
|
||||
expect(changed.changed, isTrue);
|
||||
expect(changed.compared, isFalse);
|
||||
expect(changed.code, aliceCipher.safetyCodeWith(mallorys));
|
||||
|
||||
// The instance hands out other keys again before the person trusts the
|
||||
// ones they compared: those compared are pinned, and these differ.
|
||||
server.handOutKeys('bob', eves);
|
||||
await alice.trustNewKeys('bob', changed.keys);
|
||||
final after = (await alice.trustIn(space)).single;
|
||||
expect(after.changed, isTrue);
|
||||
expect(after.code, aliceCipher.safetyCodeWith(eves));
|
||||
|
||||
server.handOutKeys('bob', mallorys);
|
||||
final trusted = (await alice.trustIn(space)).single;
|
||||
expect(trusted.changed, isFalse);
|
||||
expect(trusted.compared, isTrue);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue