refactor!: Rename sync APIs and ownership spaces (#128)
This commit is contained in:
parent
4331daf661
commit
b89725f6ea
5 changed files with 43650 additions and 9417 deletions
|
|
@ -1,358 +0,0 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT scopes]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
session.serverpod.initializeCrdtSync(syncTables: []);
|
||||
});
|
||||
|
||||
group('Given a database with no shared scopes,', () {
|
||||
test(
|
||||
'when createFor is called with only a user, '
|
||||
'then the user is granted readWrite access to the new scope.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.createFor(user);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await CrdtScopeMembership.memberScopes(session, user),
|
||||
contains(scope),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called with a user and a role, '
|
||||
'then the user is granted that role.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.createFor(
|
||||
user,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called without grants, '
|
||||
'then the new shared scope is dormant and has no explicit members.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.create();
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), isEmpty);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await CrdtScopeMembership.memberScopes(session, user),
|
||||
isNot(contains(scope)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called with several grants, '
|
||||
'then every membership is stored with its role.',
|
||||
() async {
|
||||
final writer = const Uuid().v7obj();
|
||||
final reader = const Uuid().v7obj();
|
||||
|
||||
final scope = await session.crdt.scopes.create(
|
||||
grants: {
|
||||
writer: CrdtScopeRole.readWrite,
|
||||
reader: CrdtScopeRole.readOnly,
|
||||
},
|
||||
);
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), {
|
||||
writer: CrdtScopeRole.readWrite,
|
||||
reader: CrdtScopeRole.readOnly,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for a personal scope with no row, '
|
||||
'then readWrite is returned.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: user),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(await session.crdt.scopes.members(user), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called inside a transaction that rolls back, '
|
||||
'then no scope or membership rows leak.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.crdt.scopes.createFor(user, transaction: tx);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called for an unknown shared scope, '
|
||||
'then CrdtScopeNotFoundException is thrown.',
|
||||
() async {
|
||||
final missingScope = const Uuid().v7obj();
|
||||
|
||||
await expectLater(
|
||||
session.crdt.scopes.grant(
|
||||
scope: missingScope,
|
||||
user: const Uuid().v7obj(),
|
||||
role: CrdtScopeRole.readWrite,
|
||||
),
|
||||
throwsA(
|
||||
isA<CrdtScopeNotFoundException>().having(
|
||||
(error) => error.scope,
|
||||
'scope',
|
||||
missingScope,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a dormant shared scope,', () {
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
scope = await session.crdt.scopes.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called, '
|
||||
'then the membership is inserted.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readOnly,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grantAll is called with several users, '
|
||||
'then all memberships are applied.',
|
||||
() async {
|
||||
final readOnlyUser = const Uuid().v7obj();
|
||||
final readWriteUser = const Uuid().v7obj();
|
||||
|
||||
await session.crdt.scopes.grantAll(scope, {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
|
||||
expect(await session.crdt.scopes.members(scope), {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called inside a transaction that rolls back, '
|
||||
'then no membership row leaks.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readWrite,
|
||||
transaction: tx,
|
||||
);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a readOnly member on a shared scope,', () {
|
||||
late UuidValue user;
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
user = const Uuid().v7obj();
|
||||
scope = await session.crdt.scopes.create(
|
||||
grants: {user: CrdtScopeRole.readOnly},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called again for the same member with readWrite, '
|
||||
'then the existing membership is updated.',
|
||||
() async {
|
||||
await session.crdt.scopes.grant(
|
||||
scope: scope,
|
||||
user: user,
|
||||
role: CrdtScopeRole.readWrite,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: user, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{user: CrdtScopeRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a shared scope that has one readWrite member,', () {
|
||||
late UuidValue member;
|
||||
late UuidValue nonMember;
|
||||
late UuidValue scope;
|
||||
|
||||
setUp(() async {
|
||||
member = const Uuid().v7obj();
|
||||
nonMember = const Uuid().v7obj();
|
||||
scope = await session.crdt.scopes.createFor(member);
|
||||
});
|
||||
|
||||
test(
|
||||
'when revoke is called for the member, '
|
||||
'then the membership is removed.',
|
||||
() async {
|
||||
await session.crdt.scopes.revoke(scope: scope, user: member);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: member, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
expect(await session.crdt.scopes.members(scope), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when revoke is called for a non-member, '
|
||||
'then it completes without changing the scope members.',
|
||||
() async {
|
||||
await expectLater(
|
||||
session.crdt.scopes.revoke(scope: scope, user: nonMember),
|
||||
completes,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.crdt.scopes.members(scope),
|
||||
{member: CrdtScopeRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for stored and missing memberships, '
|
||||
'then it returns the stored role or null.',
|
||||
() async {
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: member, scope: scope),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.crdt.scopes.roleOf(user: nonMember, scope: scope),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with populated and dormant shared scopes,', () {
|
||||
late UuidValue readOnlyUser;
|
||||
late UuidValue readWriteUser;
|
||||
late UuidValue populatedScope;
|
||||
late UuidValue dormantScope;
|
||||
|
||||
setUp(() async {
|
||||
readOnlyUser = const Uuid().v7obj();
|
||||
readWriteUser = const Uuid().v7obj();
|
||||
populatedScope = await session.crdt.scopes.create(
|
||||
grants: {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
},
|
||||
);
|
||||
dormantScope = await session.crdt.scopes.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when members is called, '
|
||||
'then explicit membership maps are returned.',
|
||||
() async {
|
||||
expect(await session.crdt.scopes.members(populatedScope), {
|
||||
readOnlyUser: CrdtScopeRole.readOnly,
|
||||
readWriteUser: CrdtScopeRole.readWrite,
|
||||
});
|
||||
expect(await session.crdt.scopes.members(dormantScope), isEmpty);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, int>> _membershipRowCounts(Session session) async {
|
||||
return {
|
||||
CrdtScope.t.tableName: await CrdtScope.db.count(session),
|
||||
CrdtScopeMember.t.tableName: await CrdtScopeMember.db.count(session),
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,358 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT spaces]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
session.serverpod.initializeOfflineSync(syncTables: []);
|
||||
});
|
||||
|
||||
group('Given a database with no shared spaces,', () {
|
||||
test(
|
||||
'when createFor is called with only a user, '
|
||||
'then the user is granted readWrite access to the new space.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.createFor(user);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.memberSpaces(session, user),
|
||||
contains(space),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called with a user and a role, '
|
||||
'then the user is granted that role.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.createFor(
|
||||
user,
|
||||
role: OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called without grants, '
|
||||
'then the new shared space is dormant and has no explicit members.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.create();
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), isEmpty);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
isNull,
|
||||
);
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.memberSpaces(session, user),
|
||||
isNot(contains(space)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when create is called with several grants, '
|
||||
'then every membership is stored with its role.',
|
||||
() async {
|
||||
final writer = const Uuid().v7obj();
|
||||
final reader = const Uuid().v7obj();
|
||||
|
||||
final space = await session.offlineSync.spaces.create(
|
||||
grants: {
|
||||
writer: OfflineSyncSpaceRole.readWrite,
|
||||
reader: OfflineSyncSpaceRole.readOnly,
|
||||
},
|
||||
);
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), {
|
||||
writer: OfflineSyncSpaceRole.readWrite,
|
||||
reader: OfflineSyncSpaceRole.readOnly,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for a personal space with no row, '
|
||||
'then readWrite is returned.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: user),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(await session.offlineSync.spaces.members(user), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when createFor is called inside a transaction that rolls back, '
|
||||
'then no space or membership rows leak.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.offlineSync.spaces.createFor(user, transaction: tx);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called for an unknown shared space, '
|
||||
'then OfflineSyncSpaceNotFoundException is thrown.',
|
||||
() async {
|
||||
final missingSpace = const Uuid().v7obj();
|
||||
|
||||
await expectLater(
|
||||
session.offlineSync.spaces.grant(
|
||||
space: missingSpace,
|
||||
user: const Uuid().v7obj(),
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
),
|
||||
throwsA(
|
||||
isA<OfflineSyncSpaceNotFoundException>().having(
|
||||
(error) => error.space,
|
||||
'space',
|
||||
missingSpace,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a dormant shared space,', () {
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
space = await session.offlineSync.spaces.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called, '
|
||||
'then the membership is inserted.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grantAll is called with several users, '
|
||||
'then all memberships are applied.',
|
||||
() async {
|
||||
final readOnlyUser = const Uuid().v7obj();
|
||||
final readWriteUser = const Uuid().v7obj();
|
||||
|
||||
await session.offlineSync.spaces.grantAll(space, {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
|
||||
expect(await session.offlineSync.spaces.members(space), {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when grant is called inside a transaction that rolls back, '
|
||||
'then no membership row leaks.',
|
||||
() async {
|
||||
final user = const Uuid().v7obj();
|
||||
final countsBefore = await _membershipRowCounts(session);
|
||||
|
||||
await expectLater(
|
||||
session.db.transaction((tx) async {
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
transaction: tx,
|
||||
);
|
||||
throw StateError('rollback');
|
||||
}),
|
||||
throwsStateError,
|
||||
);
|
||||
|
||||
expect(await _membershipRowCounts(session), countsBefore);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a readOnly member on a shared space,', () {
|
||||
late UuidValue user;
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
user = const Uuid().v7obj();
|
||||
space = await session.offlineSync.spaces.create(
|
||||
grants: {user: OfflineSyncSpaceRole.readOnly},
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when grant is called again for the same member with readWrite, '
|
||||
'then the existing membership is updated.',
|
||||
() async {
|
||||
await session.offlineSync.spaces.grant(
|
||||
space: space,
|
||||
user: user,
|
||||
role: OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: user, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{user: OfflineSyncSpaceRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a shared space that has one readWrite member,', () {
|
||||
late UuidValue member;
|
||||
late UuidValue nonMember;
|
||||
late UuidValue space;
|
||||
|
||||
setUp(() async {
|
||||
member = const Uuid().v7obj();
|
||||
nonMember = const Uuid().v7obj();
|
||||
space = await session.offlineSync.spaces.createFor(member);
|
||||
});
|
||||
|
||||
test(
|
||||
'when revoke is called for the member, '
|
||||
'then the membership is removed.',
|
||||
() async {
|
||||
await session.offlineSync.spaces.revoke(space: space, user: member);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: member, space: space),
|
||||
isNull,
|
||||
);
|
||||
expect(await session.offlineSync.spaces.members(space), isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when revoke is called for a non-member, '
|
||||
'then it completes without changing the space members.',
|
||||
() async {
|
||||
await expectLater(
|
||||
session.offlineSync.spaces.revoke(space: space, user: nonMember),
|
||||
completes,
|
||||
);
|
||||
|
||||
expect(
|
||||
await session.offlineSync.spaces.members(space),
|
||||
{member: OfflineSyncSpaceRole.readWrite},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when roleOf is called for stored and missing memberships, '
|
||||
'then it returns the stored role or null.',
|
||||
() async {
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: member, space: space),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
expect(
|
||||
await session.offlineSync.spaces.roleOf(user: nonMember, space: space),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with populated and dormant shared spaces,', () {
|
||||
late UuidValue readOnlyUser;
|
||||
late UuidValue readWriteUser;
|
||||
late UuidValue populatedSpace;
|
||||
late UuidValue dormantSpace;
|
||||
|
||||
setUp(() async {
|
||||
readOnlyUser = const Uuid().v7obj();
|
||||
readWriteUser = const Uuid().v7obj();
|
||||
populatedSpace = await session.offlineSync.spaces.create(
|
||||
grants: {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
},
|
||||
);
|
||||
dormantSpace = await session.offlineSync.spaces.create();
|
||||
});
|
||||
|
||||
test(
|
||||
'when members is called, '
|
||||
'then explicit membership maps are returned.',
|
||||
() async {
|
||||
expect(await session.offlineSync.spaces.members(populatedSpace), {
|
||||
readOnlyUser: OfflineSyncSpaceRole.readOnly,
|
||||
readWriteUser: OfflineSyncSpaceRole.readWrite,
|
||||
});
|
||||
expect(await session.offlineSync.spaces.members(dormantSpace), isEmpty);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<String, int>> _membershipRowCounts(Session session) async {
|
||||
return {
|
||||
OfflineSyncSpace.t.tableName: await OfflineSyncSpace.db.count(session),
|
||||
OfflineSyncSpaceMember.t.tableName: await OfflineSyncSpaceMember.db.count(session),
|
||||
};
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:serverpod_offline_sync_server/src/generated/protocol.dart' as server;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT Scope Membership]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
|
||||
group(
|
||||
'Given a user with personal scope access and readWrite membership in one shared CRDT scope,',
|
||||
() {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
|
||||
final sharedScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: sharedScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readWrite,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when member scopes are resolved, '
|
||||
'then only the personal scope and the granted shared scope are included.',
|
||||
() async {
|
||||
final scopes = await CrdtScopeMembership.memberScopes(session, userUuid);
|
||||
final expectedScopes = [userUuid, sharedScopeUuid]
|
||||
..sort((a, b) => a.uuid.compareTo(b.uuid));
|
||||
|
||||
expect(scopes, expectedScopes);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when member grants are resolved, '
|
||||
'then the personal and granted shared scopes are returned with their roles.',
|
||||
() async {
|
||||
final grants = await CrdtScopeMembership.memberGrants(session, userUuid);
|
||||
|
||||
expect(
|
||||
{for (final grant in grants) grant.uuidScopeId: grant.role},
|
||||
{
|
||||
userUuid: CrdtScopeRole.readWrite,
|
||||
sharedScopeUuid: CrdtScopeRole.readWrite,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the personal scope, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: userUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the granted shared scope, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the granted shared scope, '
|
||||
'then the stored membership role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the personal scope, '
|
||||
'then the readWrite role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: userUuid,
|
||||
),
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for a scope without a CRDT scope row, '
|
||||
'then no role is returned.',
|
||||
() async {
|
||||
final missingScopeUuid = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: missingScopeUuid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stray membership row grants readOnly access to the personal scope, '
|
||||
'then the personal grant remains readWrite.',
|
||||
() async {
|
||||
final personalScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: userUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: personalScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readOnly,
|
||||
),
|
||||
);
|
||||
|
||||
final grants = await CrdtScopeMembership.memberGrants(session, userUuid);
|
||||
|
||||
expect(
|
||||
grants.singleWhere((grant) => grant.uuidScopeId == userUuid).role,
|
||||
CrdtScopeRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stored membership row has no role, '
|
||||
'then resolving grants fails instead of treating it as a grant.',
|
||||
() async {
|
||||
final scope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: const Uuid().v7obj()),
|
||||
);
|
||||
final encodedScopeId = ValueEncoder.instance.convert(scope.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
|
||||
await session.db.unsafeExecute(
|
||||
'ALTER TABLE "crdt_scope_members" ALTER COLUMN "role" DROP NOT NULL',
|
||||
);
|
||||
await session.db.unsafeExecute(
|
||||
'INSERT INTO "crdt_scope_members" ("scopeId", "userUuid", "role") '
|
||||
'VALUES ($encodedScopeId, $encodedUserUuid, NULL)',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
CrdtScopeMembership.memberGrants(session, userUuid),
|
||||
throwsA(isA<Error>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Given a user with readOnly membership in a shared CRDT scope,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
final sharedScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: sharedScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readOnly,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when the role is resolved for that scope, '
|
||||
'then the readOnly role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
CrdtScopeRole.readOnly,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a shared CRDT scope without membership for a user,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when membership is checked for that scope, '
|
||||
'then the user is not a member.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
import 'package:serverpod/serverpod.dart';
|
||||
import 'package:serverpod_offline_sync_server/serverpod_offline_sync_server.dart';
|
||||
import 'package:serverpod_offline_sync_server/src/generated/protocol.dart' as server;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod('[CRDT Space Membership]', (sessionBuilder, _) {
|
||||
late Session session;
|
||||
|
||||
setUp(() {
|
||||
session = sessionBuilder.build();
|
||||
});
|
||||
|
||||
group(
|
||||
'Given a user with personal space access and readWrite membership in one shared CRDT space,',
|
||||
() {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
|
||||
final sharedSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: sharedSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readWrite,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when member spaces are resolved, '
|
||||
'then only the personal space and the granted shared space are included.',
|
||||
() async {
|
||||
final spaces = await OfflineSyncSpaceMembership.memberSpaces(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
final expectedSpaces = [userUuid, sharedSpaceUuid]
|
||||
..sort((a, b) => a.uuid.compareTo(b.uuid));
|
||||
|
||||
expect(spaces, expectedSpaces);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when member grants are resolved, '
|
||||
'then the personal and granted shared spaces are returned with their roles.',
|
||||
() async {
|
||||
final grants = await OfflineSyncSpaceMembership.memberGrants(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
|
||||
expect(
|
||||
{for (final grant in grants) grant.uuidSpaceId: grant.role},
|
||||
{
|
||||
userUuid: OfflineSyncSpaceRole.readWrite,
|
||||
sharedSpaceUuid: OfflineSyncSpaceRole.readWrite,
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the personal space, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: userUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when membership is checked for the granted shared space, '
|
||||
'then the user is a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the granted shared space, '
|
||||
'then the stored membership role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for the personal space, '
|
||||
'then the readWrite role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: userUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when the role is resolved for a space without a CRDT space row, '
|
||||
'then no role is returned.',
|
||||
() async {
|
||||
final missingSpaceUuid = const Uuid().v7obj();
|
||||
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: missingSpaceUuid,
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stray membership row grants readOnly access to the personal space, '
|
||||
'then the personal grant remains readWrite.',
|
||||
() async {
|
||||
final personalSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: userUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: personalSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readOnly,
|
||||
),
|
||||
);
|
||||
|
||||
final grants = await OfflineSyncSpaceMembership.memberGrants(
|
||||
session,
|
||||
userUuid,
|
||||
);
|
||||
|
||||
expect(
|
||||
grants.singleWhere((grant) => grant.uuidSpaceId == userUuid).role,
|
||||
OfflineSyncSpaceRole.readWrite,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'when a stored membership row has no role, '
|
||||
'then resolving grants fails instead of treating it as a grant.',
|
||||
() async {
|
||||
final space = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: const Uuid().v7obj()),
|
||||
);
|
||||
final encodedSpaceId = ValueEncoder.instance.convert(space.id);
|
||||
final encodedUserUuid = ValueEncoder.instance.convert(userUuid);
|
||||
|
||||
await session.db.unsafeExecute(
|
||||
'ALTER TABLE "offline_sync_space_members" ALTER COLUMN "role" DROP NOT NULL',
|
||||
);
|
||||
await session.db.unsafeExecute(
|
||||
'INSERT INTO "offline_sync_space_members" ("spaceId", "userUuid", "role") '
|
||||
'VALUES ($encodedSpaceId, $encodedUserUuid, NULL)',
|
||||
);
|
||||
|
||||
await expectLater(
|
||||
OfflineSyncSpaceMembership.memberGrants(session, userUuid),
|
||||
throwsA(isA<Error>()),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Given a user with readOnly membership in a shared CRDT space,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
final sharedSpace = await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
await server.OfflineSyncSpaceMember.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpaceMember(
|
||||
spaceId: sharedSpace.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.OfflineSyncSpaceRole.readOnly,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when the role is resolved for that space, '
|
||||
'then the readOnly role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.roleOf(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
OfflineSyncSpaceRole.readOnly,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a shared CRDT space without membership for a user,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedSpaceUuid;
|
||||
|
||||
setUp(() async {
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedSpaceUuid = const Uuid().v7obj();
|
||||
await server.OfflineSyncSpace.db.insertRow(
|
||||
session,
|
||||
server.OfflineSyncSpace(uuidSpaceId: sharedSpaceUuid),
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'when membership is checked for that space, '
|
||||
'then the user is not a member.',
|
||||
() async {
|
||||
expect(
|
||||
await OfflineSyncSpaceMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
spaceUuid: sharedSpaceUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ void withServerpod(
|
|||
}
|
||||
|
||||
class TestEndpoints {
|
||||
late final _CrdtSyncEndpoint crdtSync;
|
||||
late final _OfflineSyncEndpoint offlineSync;
|
||||
}
|
||||
|
||||
class _InternalTestEndpoints extends TestEndpoints
|
||||
|
|
@ -154,15 +154,15 @@ class _InternalTestEndpoints extends TestEndpoints
|
|||
_is.SerializationManager serializationManager,
|
||||
_is.EndpointDispatch endpoints,
|
||||
) {
|
||||
crdtSync = _CrdtSyncEndpoint(
|
||||
offlineSync = _OfflineSyncEndpoint(
|
||||
endpoints,
|
||||
serializationManager,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CrdtSyncEndpoint {
|
||||
_CrdtSyncEndpoint(
|
||||
class _OfflineSyncEndpoint {
|
||||
_OfflineSyncEndpoint(
|
||||
this._endpointDispatch,
|
||||
this._serializationManager,
|
||||
);
|
||||
|
|
@ -171,23 +171,23 @@ class _CrdtSyncEndpoint {
|
|||
|
||||
final _is.SerializationManager _serializationManager;
|
||||
|
||||
_ida.Stream<_icw2tu00.CrdtSyncStreamEvent> sync(
|
||||
_ida.Stream<_icw2tu00.OfflineSyncStreamEvent> sync(
|
||||
_ist.TestSessionBuilder sessionBuilder, {
|
||||
required _ida.Stream<_icw2tu00.CrdtSyncStreamEvent> changes,
|
||||
required _ida.Stream<_icw2tu00.OfflineSyncStreamEvent> changes,
|
||||
required bool once,
|
||||
}) {
|
||||
var _localTestStreamManager =
|
||||
_ist.TestStreamManager<_icw2tu00.CrdtSyncStreamEvent>();
|
||||
_ist.TestStreamManager<_icw2tu00.OfflineSyncStreamEvent>();
|
||||
_ist.callStreamFunctionAndHandleExceptions(
|
||||
() async {
|
||||
var _localUniqueSession = (sessionBuilder as _ist.InternalTestSessionBuilder)
|
||||
.internalBuild(
|
||||
endpoint: 'crdtSync',
|
||||
endpoint: 'offlineSync',
|
||||
method: 'sync',
|
||||
);
|
||||
var _localCallContext = await _endpointDispatch.getMethodStreamCallContext(
|
||||
createSessionCallback: (_) => _localUniqueSession,
|
||||
endpointPath: 'crdtSync',
|
||||
endpointPath: 'offlineSync',
|
||||
methodName: 'sync',
|
||||
arguments: {'once': once},
|
||||
requestedInputStreams: ['changes'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue