feat(base,app,learn): a shared space's key rolls once a member goes
What is written in a shared space after a member went takes a key they
never get (Halkyon's ADR 0044, D22). The instance marks the space as one
of its members goes, removed, leaving or deleted (`keyRollDue` on
`shared_space`, set in `removeMember`, through which each of them
passes); an invitation taken back never had a key and marks nothing, nor
does a new role. The Owner's or an Editor's device that next keeps its
keys up after a sync is told so (`sharedSpaceKeys.rollDue`, never true
for a Viewer, who seals no key) and makes a new key, sealed to those who
stay (D33). The first seal bringing a key ID the space never had clears
the mark in the same transaction (D34); members the rolling device
missed get it later as newcomers do. Earlier rows keep the key they had,
which whoever went had read already.
The migration, the same in the module and in each server including it
(Learn, Meditate, Todo, Train):
ALTER TABLE "shared_space"
ADD COLUMN "keyRollDue" boolean NOT NULL DEFAULT false;
It adds the column with false for every space there is, so no space is
due a roll from it; PostgreSQL adds a column with a constant default
without rewriting the table. The server before the switch reads and
writes the table naming its own columns, so it keeps working.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b45cb0f3cb
commit
7f1f07883a
21 changed files with 100883 additions and 6 deletions
|
|
@ -46,5 +46,9 @@ class ServerpodSharedKeysGateway(final protocol.Caller _base)
|
|||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<bool> rollDue(String space) =>
|
||||
_base.sharedSpaceKeys.rollDue(_id(space));
|
||||
|
||||
protocol.UuidValue _id(String id) => protocol.UuidValue.withValidation(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ class SharedKeys(
|
|||
);
|
||||
}
|
||||
|
||||
/// Whether [space] is due a new key, as a member went since its latest
|
||||
/// (ADR 0044, D22, D33): a writer's device then makes one and [share]s
|
||||
/// it. Never for a Viewer.
|
||||
Future<bool> rollDue(String space) => _gateway.rollDue(space);
|
||||
|
||||
/// Seals the keys of [space] that this device holds to the members who
|
||||
/// lack them: newcomers, and those who have keys only now. Nothing for a
|
||||
/// Viewer, whom the instance asks for none.
|
||||
|
|
|
|||
|
|
@ -27,4 +27,8 @@ abstract interface class SharedKeysGateway() {
|
|||
|
||||
/// The keys of [space] sealed to the person, each naming its sealer.
|
||||
Future<List<SealedKey>> mine(String space);
|
||||
|
||||
/// Whether [space] is due a new key, a member having gone since its
|
||||
/// latest (ADR 0044, D22); never for a Viewer.
|
||||
Future<bool> rollDue(String space);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,15 @@ class FakeSharedKeysServer() {
|
|||
handle: handle,
|
||||
);
|
||||
|
||||
/// Ends [person]'s membership of [space], with the keys sealed to them.
|
||||
/// The spaces due a new key, a member having gone (ADR 0044, D22).
|
||||
final _rollDue = <String>{};
|
||||
|
||||
/// Ends [person]'s membership of [space], with the keys sealed to them;
|
||||
/// the space is due a new key.
|
||||
void removeMember(String space, String person) {
|
||||
_members[space]?.remove(person);
|
||||
_sealed[space]?.removeWhere((key) => key.to == person);
|
||||
_rollDue.add(space);
|
||||
}
|
||||
|
||||
/// The keys handed out for [person] from now on, in every space.
|
||||
|
|
@ -110,6 +115,11 @@ class _View(final FakeSharedKeysServer _server, final String _person)
|
|||
throw StateError('Not allowed');
|
||||
}
|
||||
final sealed = _server._sealed[space] ??= [];
|
||||
// A key the space never had is the one a roll made (D34).
|
||||
final known = {for (final key in sealed) key.keyId};
|
||||
if (keys.any((key) => !known.contains(key.keyId))) {
|
||||
_server._rollDue.remove(space);
|
||||
}
|
||||
for (final key in keys) {
|
||||
if (!members.containsKey(key.person)) {
|
||||
throw StateError('No such member');
|
||||
|
|
@ -125,6 +135,12 @@ class _View(final FakeSharedKeysServer _server, final String _person)
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> rollDue(String space) async {
|
||||
final members = _server._membersAs(space, _person);
|
||||
return members[_person]!.writes && _server._rollDue.contains(space);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SealedKey>> mine(String space) async {
|
||||
_server._membersAs(space, _person);
|
||||
|
|
|
|||
|
|
@ -193,6 +193,32 @@ void main() {
|
|||
expect((await alice.sealForNewcomers(space)).sealedTo, isEmpty);
|
||||
});
|
||||
|
||||
test('a member going makes the space due a new key, for writers alone, '
|
||||
'which the first key it never had ends (D22, D33, D34)', () async {
|
||||
aliceCipher.newKey('k1');
|
||||
await alice.share(space, 'k1');
|
||||
server.addMember(space, 'carol', writes: false);
|
||||
expect(await alice.rollDue(space), isFalse);
|
||||
|
||||
server.removeMember(space, 'bob');
|
||||
expect(await alice.rollDue(space), isTrue);
|
||||
expect(
|
||||
await SharedKeys(
|
||||
server.as('carol'),
|
||||
KeyPins(MemoryKeyPinStore()),
|
||||
FakeContentCipher(),
|
||||
me: 'carol',
|
||||
).rollDue(space),
|
||||
isFalse,
|
||||
);
|
||||
|
||||
await alice.share(space, 'k1');
|
||||
expect(await alice.rollDue(space), isTrue, reason: 'not a new key');
|
||||
aliceCipher.newKey('k2');
|
||||
await alice.share(space, 'k2');
|
||||
expect(await alice.rollDue(space), isFalse);
|
||||
});
|
||||
|
||||
test("a key of a member's who left opens with the keys pinned for "
|
||||
'them', () async {
|
||||
aliceCipher.newKey('k1');
|
||||
|
|
|
|||
|
|
@ -1486,6 +1486,7 @@ class EndpointSharedSpaceKeys extends _isc.EndpointRef {
|
|||
/// first of two the call names alike. Refused for a
|
||||
/// Viewer: only those who write, the Owner and Editors, seal keys, so a
|
||||
/// reader can't hand a newcomer a key of their own making.
|
||||
/// A key the space never had ends its being due a new one ([rollDue]).
|
||||
_ida.Future<void> seal(
|
||||
_isc.UuidValue space,
|
||||
List<_ie2by8eg.SharedSpaceSealedKey> keys,
|
||||
|
|
@ -1498,6 +1499,16 @@ class EndpointSharedSpaceKeys extends _isc.EndpointRef {
|
|||
},
|
||||
);
|
||||
|
||||
/// Whether [space] is due a new key, as a member went since its latest
|
||||
/// was made (ADR 0044, D22, D33): for your device to make one and seal
|
||||
/// it to the members. Never for a Viewer, who seals none ([seal]).
|
||||
_ida.Future<bool> rollDue(_isc.UuidValue space) =>
|
||||
caller.callServerEndpoint<bool>(
|
||||
'halkyon_base.sharedSpaceKeys',
|
||||
'rollDue',
|
||||
{'space': space},
|
||||
);
|
||||
|
||||
/// The keys of [space] sealed to you, each naming who sealed it.
|
||||
_ida.Future<List<_ie2by8eg.SharedSpaceSealedKey>> mine(
|
||||
_isc.UuidValue space,
|
||||
|
|
|
|||
|
|
@ -2216,6 +2216,27 @@ class Endpoints extends _is.EndpointDispatch {
|
|||
params['keys'],
|
||||
),
|
||||
),
|
||||
'rollDue': _is.MethodConnector(
|
||||
name: 'rollDue',
|
||||
params: {
|
||||
'space': _is.ParameterDescription(
|
||||
name: 'space',
|
||||
type: _is.getType<_is.UuidValue>(),
|
||||
nullable: false,
|
||||
),
|
||||
},
|
||||
call:
|
||||
(
|
||||
_is.Session session,
|
||||
Map<String, dynamic> params,
|
||||
) async =>
|
||||
(endpoints['sharedSpaceKeys']
|
||||
as _izzpx5a5.SharedSpaceKeysEndpoint)
|
||||
.rollDue(
|
||||
session,
|
||||
params['space'],
|
||||
),
|
||||
),
|
||||
'mine': _is.MethodConnector(
|
||||
name: 'mine',
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -1323,6 +1323,13 @@ class Protocol extends _is.DatabaseSerializationManager {
|
|||
dartType: 'bool',
|
||||
columnDefault: 'false',
|
||||
),
|
||||
_isp.ColumnDefinition(
|
||||
name: 'keyRollDue',
|
||||
columnType: _isp.ColumnType.boolean,
|
||||
isNullable: false,
|
||||
dartType: 'bool',
|
||||
columnDefault: 'false',
|
||||
),
|
||||
],
|
||||
foreignKeys: [],
|
||||
indexes: [],
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ sharedSpaceKeys:
|
|||
- memberKeys:
|
||||
- keysNeeded:
|
||||
- seal:
|
||||
- rollDue:
|
||||
- mine:
|
||||
simpleLogin:
|
||||
- begin:
|
||||
|
|
|
|||
|
|
@ -24,8 +24,10 @@ abstract class SharedSpace
|
|||
required this.createdAt,
|
||||
bool? joinByLink,
|
||||
bool? membersInvite,
|
||||
bool? keyRollDue,
|
||||
}) : joinByLink = joinByLink ?? false,
|
||||
membersInvite = membersInvite ?? false;
|
||||
membersInvite = membersInvite ?? false,
|
||||
keyRollDue = keyRollDue ?? false;
|
||||
|
||||
factory SharedSpace({
|
||||
_is.UuidValue? id,
|
||||
|
|
@ -33,6 +35,7 @@ abstract class SharedSpace
|
|||
required DateTime createdAt,
|
||||
bool? joinByLink,
|
||||
bool? membersInvite,
|
||||
bool? keyRollDue,
|
||||
}) = _SharedSpaceImpl;
|
||||
|
||||
factory SharedSpace.fromJson(Map<String, dynamic> jsonSerialization) {
|
||||
|
|
@ -50,6 +53,9 @@ abstract class SharedSpace
|
|||
membersInvite: jsonSerialization['membersInvite'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['membersInvite']),
|
||||
keyRollDue: jsonSerialization['keyRollDue'] == null
|
||||
? null
|
||||
: _is.BoolJsonExtension.fromJson(jsonSerialization['keyRollDue']),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -72,6 +78,11 @@ abstract class SharedSpace
|
|||
/// first.
|
||||
bool membersInvite;
|
||||
|
||||
/// Whether a member went since the space's latest key was made (ADR
|
||||
/// 0044, D22, D33): the next writer's device makes a new one, which
|
||||
/// clears it (D34).
|
||||
bool keyRollDue;
|
||||
|
||||
@override
|
||||
_is.Table<_is.UuidValue?> get table => t;
|
||||
|
||||
|
|
@ -84,6 +95,7 @@ abstract class SharedSpace
|
|||
DateTime? createdAt,
|
||||
bool? joinByLink,
|
||||
bool? membersInvite,
|
||||
bool? keyRollDue,
|
||||
});
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
|
|
@ -94,6 +106,7 @@ abstract class SharedSpace
|
|||
'createdAt': createdAt.toJson(),
|
||||
'joinByLink': joinByLink,
|
||||
'membersInvite': membersInvite,
|
||||
'keyRollDue': keyRollDue,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -139,12 +152,14 @@ class _SharedSpaceImpl extends SharedSpace {
|
|||
required DateTime createdAt,
|
||||
bool? joinByLink,
|
||||
bool? membersInvite,
|
||||
bool? keyRollDue,
|
||||
}) : super._(
|
||||
id: id,
|
||||
kind: kind,
|
||||
createdAt: createdAt,
|
||||
joinByLink: joinByLink,
|
||||
membersInvite: membersInvite,
|
||||
keyRollDue: keyRollDue,
|
||||
);
|
||||
|
||||
/// Returns a shallow copy of this [SharedSpace]
|
||||
|
|
@ -157,6 +172,7 @@ class _SharedSpaceImpl extends SharedSpace {
|
|||
DateTime? createdAt,
|
||||
bool? joinByLink,
|
||||
bool? membersInvite,
|
||||
bool? keyRollDue,
|
||||
}) {
|
||||
return SharedSpace(
|
||||
id: id is _is.UuidValue? ? id : this.id,
|
||||
|
|
@ -164,6 +180,7 @@ class _SharedSpaceImpl extends SharedSpace {
|
|||
createdAt: createdAt ?? this.createdAt,
|
||||
joinByLink: joinByLink ?? this.joinByLink,
|
||||
membersInvite: membersInvite ?? this.membersInvite,
|
||||
keyRollDue: keyRollDue ?? this.keyRollDue,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -191,6 +208,11 @@ class SharedSpaceUpdateTable extends _is.UpdateTable<SharedSpaceTable> {
|
|||
table.membersInvite,
|
||||
value,
|
||||
);
|
||||
|
||||
_is.ColumnValue<bool, bool> keyRollDue(bool value) => _is.ColumnValue(
|
||||
table.keyRollDue,
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
class SharedSpaceTable extends _is.Table<_is.UuidValue?> {
|
||||
|
|
@ -214,6 +236,11 @@ class SharedSpaceTable extends _is.Table<_is.UuidValue?> {
|
|||
this,
|
||||
hasDefault: true,
|
||||
);
|
||||
keyRollDue = _is.ColumnBool(
|
||||
'keyRollDue',
|
||||
this,
|
||||
hasDefault: true,
|
||||
);
|
||||
}
|
||||
|
||||
late final SharedSpaceUpdateTable updateTable;
|
||||
|
|
@ -230,6 +257,11 @@ class SharedSpaceTable extends _is.Table<_is.UuidValue?> {
|
|||
/// first.
|
||||
late final _is.ColumnBool membersInvite;
|
||||
|
||||
/// Whether a member went since the space's latest key was made (ADR
|
||||
/// 0044, D22, D33): the next writer's device makes a new one, which
|
||||
/// clears it (D34).
|
||||
late final _is.ColumnBool keyRollDue;
|
||||
|
||||
@override
|
||||
List<_is.Column> get columns => [
|
||||
id,
|
||||
|
|
@ -237,6 +269,7 @@ class SharedSpaceTable extends _is.Table<_is.UuidValue?> {
|
|||
createdAt,
|
||||
joinByLink,
|
||||
membersInvite,
|
||||
keyRollDue,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,3 +16,7 @@ fields:
|
|||
### Whether members other than the Owner may invite others (D5); off at
|
||||
### first.
|
||||
membersInvite: bool, default=false
|
||||
### Whether a member went since the space's latest key was made (ADR
|
||||
### 0044, D22, D33): the next writer's device makes a new one, which
|
||||
### clears it (D34).
|
||||
keyRollDue: bool, default=false
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ class SharedSpaceKeysEndpoint extends Endpoint {
|
|||
/// first of two the call names alike. Refused for a
|
||||
/// Viewer: only those who write, the Owner and Editors, seal keys, so a
|
||||
/// reader can't hand a newcomer a key of their own making.
|
||||
/// A key the space never had ends its being due a new one ([rollDue]).
|
||||
Future<void> seal(
|
||||
Session session,
|
||||
UuidValue space,
|
||||
|
|
@ -99,7 +100,7 @@ class SharedSpaceKeysEndpoint extends Endpoint {
|
|||
await session.db.transaction((transaction) async {
|
||||
// The space's row held, so two devices sealing at once take turns.
|
||||
// SELECT … FROM "shared_space" WHERE "id" = $1 FOR UPDATE
|
||||
await SharedSpace.db.findById(
|
||||
final row = await SharedSpace.db.findById(
|
||||
session,
|
||||
space,
|
||||
transaction: transaction,
|
||||
|
|
@ -129,9 +130,35 @@ class SharedSpaceKeysEndpoint extends Endpoint {
|
|||
createdAt: now,
|
||||
),
|
||||
], transaction: transaction);
|
||||
// A key the space never had is the one a roll made (D34).
|
||||
final known = {for (final (keyId, _) in there) keyId};
|
||||
if ((row?.keyRollDue ?? false) &&
|
||||
added.any((key) => !known.contains(key.$1))) {
|
||||
// UPDATE "shared_space" SET "keyRollDue" = FALSE WHERE "id" = $1
|
||||
await SharedSpace.db.updateRow(
|
||||
session,
|
||||
row!.copyWith(keyRollDue: false),
|
||||
columns: (t) => [t.keyRollDue],
|
||||
transaction: transaction,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Whether [space] is due a new key, as a member went since its latest
|
||||
/// was made (ADR 0044, D22, D33): for your device to make one and seal
|
||||
/// it to the members. Never for a Viewer, who seals none ([seal]).
|
||||
Future<bool> rollDue(Session session, UuidValue space) async {
|
||||
final person = _caller(session);
|
||||
final members = await _membersAsCaller(session, space);
|
||||
if (!_writes(members, person)) {
|
||||
return false;
|
||||
}
|
||||
// SELECT … FROM "shared_space" WHERE "id" = $1
|
||||
final row = await SharedSpace.db.findById(session, space);
|
||||
return row?.keyRollDue ?? false;
|
||||
}
|
||||
|
||||
/// The keys of [space] sealed to you, each naming who sealed it.
|
||||
Future<List<SharedSpaceSealedKey>> mine(
|
||||
Session session,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ Future<void> removeMember(
|
|||
member.authUserId,
|
||||
transaction: transaction,
|
||||
);
|
||||
// What is written from now on takes a new key, which they never get
|
||||
// (D22): the next writer's device makes it.
|
||||
// UPDATE "shared_space" SET "keyRollDue" = TRUE WHERE "id" = $1
|
||||
await SharedSpace.db.updateWhere(
|
||||
session,
|
||||
columnValues: (t) => [t.keyRollDue(true)],
|
||||
where: (t) => t.id.equals(member.sharedSpaceId),
|
||||
transaction: transaction,
|
||||
);
|
||||
// The keys sealed to them: a device of theirs opens none from here on,
|
||||
// and none that comes back after they rejoin is the first one.
|
||||
// DELETE FROM "shared_space_key"
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"__className__": "serverpod.DatabaseMigration",
|
||||
"actions": [
|
||||
{
|
||||
"__className__": "serverpod.DatabaseMigrationAction",
|
||||
"type": "alterTable",
|
||||
"alterTable": {
|
||||
"__className__": "serverpod.TableMigration",
|
||||
"name": "shared_space",
|
||||
"schema": "public",
|
||||
"addColumns": [
|
||||
{
|
||||
"__className__": "serverpod.ColumnDefinition",
|
||||
"name": "keyRollDue",
|
||||
"fieldName": "keyRollDue",
|
||||
"columnType": 1,
|
||||
"isNullable": false,
|
||||
"columnDefault": "false",
|
||||
"dartType": "bool"
|
||||
}
|
||||
],
|
||||
"deleteColumns": [],
|
||||
"modifyColumns": [],
|
||||
"addIndexes": [],
|
||||
"deleteIndexes": [],
|
||||
"addForeignKeys": [],
|
||||
"deleteForeignKeys": [],
|
||||
"warnings": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"warnings": [],
|
||||
"migrationApiVersion": 1
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
BEGIN;
|
||||
|
||||
--
|
||||
-- ACTION ALTER TABLE
|
||||
--
|
||||
ALTER TABLE "shared_space" ADD COLUMN "keyRollDue" boolean NOT NULL DEFAULT false;
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_base
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_base', '20261111145608468-shared-space-key-roll', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261111145608468-shared-space-key-roll', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR halkyon_oidc
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('halkyon_oidc', '20261023180644988', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20261023180644988', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod', '20260824182259319', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260824182259319', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_idp
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_idp', '20260924105404509', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105404509', "timestamp" = now();
|
||||
|
||||
--
|
||||
-- MIGRATION VERSION FOR serverpod_auth_core
|
||||
--
|
||||
INSERT INTO "serverpod_migrations" ("module", "version", "timestamp")
|
||||
VALUES ('serverpod_auth_core', '20260924105232991', now())
|
||||
ON CONFLICT ("module")
|
||||
DO UPDATE SET "version" = '20260924105232991', "timestamp" = now();
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -24,3 +24,4 @@
|
|||
20261110225132890-file-store
|
||||
20261111091525523-shared-spaces
|
||||
20261111100053843-shared-space-keys
|
||||
20261111145608468-shared-space-key-roll
|
||||
|
|
|
|||
|
|
@ -255,5 +255,77 @@ void main() {
|
|||
[alice],
|
||||
);
|
||||
});
|
||||
|
||||
/// The membership ID of [handle] in the space, as its Owner sees it.
|
||||
Future<UuidValue> membershipOf(String handle) async =>
|
||||
(await endpoints.sharedSpace.members(
|
||||
signedIn(alice),
|
||||
space,
|
||||
)).singleWhere((member) => member.handle == handle).id;
|
||||
|
||||
test(
|
||||
'a space whose member went is due a new key, which its writers are '
|
||||
'told; the first key the space never had ends it (D22, D33, D34)',
|
||||
() async {
|
||||
await api.seal(signedIn(alice), space, [
|
||||
sealed('k1', alice),
|
||||
sealed('k1', bob),
|
||||
]);
|
||||
expect(await api.rollDue(signedIn(alice), space), isFalse);
|
||||
|
||||
await endpoints.sharedSpace.remove(
|
||||
signedIn(alice),
|
||||
space,
|
||||
await membershipOf('carol'),
|
||||
);
|
||||
expect(await api.rollDue(signedIn(alice), space), isTrue);
|
||||
expect(await api.rollDue(signedIn(bob), space), isTrue);
|
||||
|
||||
// A key the space has already, sealed to someone else, ends nothing.
|
||||
await api.seal(signedIn(bob), space, [sealed('k1', bob)]);
|
||||
expect(await api.rollDue(signedIn(alice), space), isTrue);
|
||||
|
||||
await api.seal(signedIn(bob), space, [
|
||||
sealed('k2', alice),
|
||||
sealed('k2', bob),
|
||||
]);
|
||||
expect(await api.rollDue(signedIn(alice), space), isFalse);
|
||||
expect(await api.rollDue(signedIn(bob), space), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('a member leaving makes it due; a new role does not', () async {
|
||||
await endpoints.sharedSpace.setRole(
|
||||
signedIn(alice),
|
||||
space,
|
||||
await membershipOf('bob'),
|
||||
SharedSpaceRole.viewer,
|
||||
);
|
||||
expect(await api.rollDue(signedIn(alice), space), isFalse);
|
||||
|
||||
await endpoints.sharedSpace.leave(signedIn(carol), space);
|
||||
expect(await api.rollDue(signedIn(alice), space), isTrue);
|
||||
});
|
||||
|
||||
test('a Viewer, who seals no key, is never told to roll one', () async {
|
||||
await endpoints.sharedSpace.remove(
|
||||
signedIn(alice),
|
||||
space,
|
||||
await membershipOf('bob'),
|
||||
);
|
||||
expect(await api.rollDue(signedIn(alice), space), isTrue);
|
||||
expect(await api.rollDue(signedIn(carol), space), isFalse);
|
||||
});
|
||||
|
||||
test('whoever is outside the space learns nothing of it', () async {
|
||||
final mallory = await person('mallory');
|
||||
await expectLater(api.rollDue(signedIn(mallory), space), notFound);
|
||||
await endpoints.sharedSpace.remove(
|
||||
signedIn(alice),
|
||||
space,
|
||||
await membershipOf('bob'),
|
||||
);
|
||||
await expectLater(api.rollDue(signedIn(bob), space), notFound);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue