fix: General tests improvements and misc small fixes (#104)
This commit is contained in:
parent
a24faf39af
commit
8e7e6551e0
4 changed files with 2902 additions and 2155 deletions
|
|
@ -336,24 +336,45 @@ class CrdtDatabase implements Database {
|
|||
transaction,
|
||||
(tx) async {
|
||||
final prepared = _prepareRowsForInsert(rows, tx);
|
||||
final deletedRowIds = await _recorder.deletedRowIds(prepared.rows, tx);
|
||||
final rowsToInsert = [
|
||||
for (final row in prepared.rows)
|
||||
if (!deletedRowIds.contains(row.id)) row,
|
||||
];
|
||||
|
||||
// Tracked rows can only skip RETURNING when the prepared rows are
|
||||
// exactly what gets inserted: every id is caller-provided and a
|
||||
// conflict throws instead of silently dropping rows.
|
||||
final skipReturn =
|
||||
noReturn && !ignoreConflicts && !rows.any((row) => row.id == null);
|
||||
|
||||
final result = await _delegate.insert<T>(
|
||||
prepared.rows,
|
||||
transaction: tx,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: skipReturn,
|
||||
);
|
||||
final result = rowsToInsert.isEmpty
|
||||
? <T>[]
|
||||
: await _delegate.insert<T>(
|
||||
rowsToInsert,
|
||||
transaction: tx,
|
||||
ignoreConflicts: ignoreConflicts,
|
||||
noReturn: skipReturn,
|
||||
);
|
||||
|
||||
final insertedRows = skipReturn ? prepared.rows : result;
|
||||
final insertedRows = skipReturn ? rowsToInsert : result;
|
||||
await _recorder.afterInsert<T>(insertedRows, tx);
|
||||
|
||||
final reinsertedRows = await _reinsertTombstonedRows(
|
||||
prepared.rows,
|
||||
insertedRows,
|
||||
tx,
|
||||
deletedRowIds: deletedRowIds,
|
||||
);
|
||||
if (noReturn) return <T>[];
|
||||
_stripStampedRows(insertedRows, prepared);
|
||||
return insertedRows;
|
||||
|
||||
final affectedRows = _orderAffectedRows(
|
||||
prepared.rows,
|
||||
insertedRows,
|
||||
reinsertedRows,
|
||||
);
|
||||
_stripStampedRows(affectedRows, prepared);
|
||||
return affectedRows;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -363,30 +384,7 @@ class CrdtDatabase implements Database {
|
|||
T row, {
|
||||
Transaction? transaction,
|
||||
}) async {
|
||||
await _ensureInitialized();
|
||||
return DatabaseUtil.runInTransactionOrSavepoint(
|
||||
_delegate,
|
||||
transaction,
|
||||
(tx) async {
|
||||
final prepared = _prepareRowsForInsert([row], tx);
|
||||
final databaseRow = prepared.rows.single;
|
||||
late final T result;
|
||||
try {
|
||||
result = await _delegate.insertRow<T>(databaseRow, transaction: tx);
|
||||
} on DatabaseQueryException {
|
||||
if (!await _recorder.isDeleted(row, tx)) rethrow;
|
||||
|
||||
result = await _delegate.updateRow<T>(databaseRow, transaction: tx);
|
||||
await _recorder.afterReinsert([result], tx);
|
||||
_stripStampedRows([result], prepared);
|
||||
return result;
|
||||
}
|
||||
|
||||
await _recorder.afterInsert([result], tx);
|
||||
_stripStampedRows([result], prepared);
|
||||
return result;
|
||||
},
|
||||
);
|
||||
return (await insert<T>([row], transaction: transaction)).single;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -473,19 +471,23 @@ class CrdtDatabase implements Database {
|
|||
Future<List<T>> _reinsertTombstonedRows<T extends TableRow>(
|
||||
List<T> preparedRows,
|
||||
List<T> result,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
Transaction transaction, {
|
||||
Set<UuidValue>? deletedRowIds,
|
||||
}) async {
|
||||
final returnedIds = <Object>{
|
||||
for (final row in result)
|
||||
if (row.id != null) row.id as Object,
|
||||
};
|
||||
|
||||
final rowsToReinsert = <T>[];
|
||||
for (final row in preparedRows) {
|
||||
final rowId = row.id;
|
||||
if (rowId == null || returnedIds.contains(rowId)) continue;
|
||||
if (await _recorder.isDeleted(row, transaction)) rowsToReinsert.add(row);
|
||||
}
|
||||
final missingRows = [
|
||||
for (final row in preparedRows)
|
||||
if (row.id case final rowId? when !returnedIds.contains(rowId)) row,
|
||||
];
|
||||
deletedRowIds ??= await _recorder.deletedRowIds(missingRows, transaction);
|
||||
final rowsToReinsert = [
|
||||
for (final row in missingRows)
|
||||
if (deletedRowIds.contains(row.id)) row,
|
||||
];
|
||||
if (rowsToReinsert.isEmpty) return [];
|
||||
|
||||
final reinsertedRows = await _delegate.update<T>(
|
||||
|
|
@ -496,6 +498,26 @@ class CrdtDatabase implements Database {
|
|||
return reinsertedRows;
|
||||
}
|
||||
|
||||
List<T> _orderAffectedRows<T extends TableRow>(
|
||||
List<T> preparedRows,
|
||||
List<T> insertedRows,
|
||||
List<T> reinsertedRows,
|
||||
) {
|
||||
if (insertedRows.length + reinsertedRows.length != preparedRows.length) {
|
||||
return [...insertedRows, ...reinsertedRows];
|
||||
}
|
||||
|
||||
final reinsertedIds = {for (final row in reinsertedRows) row.id};
|
||||
final inserted = insertedRows.iterator;
|
||||
final reinserted = reinsertedRows.iterator;
|
||||
return [
|
||||
for (final row in preparedRows)
|
||||
reinsertedIds.contains(row.id)
|
||||
? (reinserted..moveNext()).current
|
||||
: (inserted..moveNext()).current,
|
||||
];
|
||||
}
|
||||
|
||||
Future<List<T>> _rowsWithoutCrdtMetadata<T extends TableRow>(
|
||||
List<T> rows,
|
||||
Transaction transaction,
|
||||
|
|
|
|||
|
|
@ -150,6 +150,13 @@ class CrdtDatabaseContext {
|
|||
final result = <String, List<_ReferencingForeignKey>>{};
|
||||
for (final table in _tableDefinitionsByName.values) {
|
||||
final childTableName = table.name;
|
||||
// Untracked children have no CRDT schema rows and cannot be
|
||||
// soft-deleted; their physical rows keep referencing the (physically
|
||||
// preserved) soft-deleted parent, so no delete action applies.
|
||||
// Interim behavior: docs/sync-non-sync-relations.md forbids
|
||||
// non-synced -> synced relations outright, but its initialize()
|
||||
// validation is not implemented yet.
|
||||
if (!_isCrdtTrackedTableName(childTableName)) continue;
|
||||
for (final foreignKey in table.foreignKeys) {
|
||||
if (foreignKey.columns.length != 1 ||
|
||||
foreignKey.referenceColumns.length != 1) {
|
||||
|
|
@ -973,22 +980,29 @@ WHERE c."id" IN ($whereRowIds)
|
|||
);
|
||||
}
|
||||
|
||||
/// Whether the given row is soft-deleted.
|
||||
Future<bool> isDeleted<T extends TableRow>(
|
||||
T row,
|
||||
/// IDs of the given rows that are soft-deleted.
|
||||
Future<Set<UuidValue>> deletedRowIds<T extends TableRow>(
|
||||
List<T> rows,
|
||||
Transaction transaction,
|
||||
) async {
|
||||
if (!isCrdtTracked<T>(row.table)) return false;
|
||||
if (row.id is! UuidValue) return false;
|
||||
final rowIds = {
|
||||
for (final row in rows)
|
||||
if (row.id is UuidValue) row.id as UuidValue,
|
||||
};
|
||||
if (rowIds.isEmpty) return {};
|
||||
|
||||
final table = rows.first.table;
|
||||
if (!isCrdtTracked<T>(table)) return {};
|
||||
|
||||
final crdtRows = await _findCrdtRows(
|
||||
row.table.tableName,
|
||||
{row.id as UuidValue},
|
||||
table.tableName,
|
||||
rowIds,
|
||||
transaction,
|
||||
);
|
||||
if (crdtRows.isEmpty) return false;
|
||||
|
||||
return crdtRows.single.isHidden;
|
||||
return {
|
||||
for (final row in crdtRows)
|
||||
if (row.isHidden) row.uuidRowId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<CrdtDataRow>> _findCrdtRows(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,25 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
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.',
|
||||
|
|
@ -128,25 +147,6 @@ void main() {
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('Given a database with a dormant shared scope,', () {
|
||||
|
|
|
|||
|
|
@ -6,135 +6,226 @@ import 'package:test/test.dart';
|
|||
import 'test_tools/serverpod_test_tools.dart';
|
||||
|
||||
void main() {
|
||||
withServerpod(
|
||||
'Given a user with personal scope access and membership in one shared CRDT scope, '
|
||||
'and another user with membership in another scope,',
|
||||
(sessionBuilder, _) {
|
||||
final userUuid = const Uuid().v7obj();
|
||||
final otherUserUuid = const Uuid().v7obj();
|
||||
late Session session;
|
||||
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<StateError>().having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
'crdt_scope_members.role must not be null.',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
group('Given a user with readOnly membership in a shared CRDT scope,', () {
|
||||
late UuidValue userUuid;
|
||||
late UuidValue sharedScopeUuid;
|
||||
late UuidValue otherScopeUuid;
|
||||
|
||||
setUp(() async {
|
||||
session = sessionBuilder.build();
|
||||
userUuid = const Uuid().v7obj();
|
||||
sharedScopeUuid = const Uuid().v7obj();
|
||||
otherScopeUuid = const Uuid().v7obj();
|
||||
|
||||
final sharedScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: sharedScopeUuid),
|
||||
);
|
||||
final otherScope = await server.CrdtScope.db.insertRow(
|
||||
session,
|
||||
server.CrdtScope(uuidScopeId: otherScopeUuid),
|
||||
);
|
||||
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: sharedScope.id!,
|
||||
userUuid: userUuid,
|
||||
role: server.CrdtScopeRole.readWrite,
|
||||
),
|
||||
);
|
||||
await server.CrdtScopeMember.db.insertRow(
|
||||
session,
|
||||
server.CrdtScopeMember(
|
||||
scopeId: otherScope.id!,
|
||||
userUuid: otherUserUuid,
|
||||
role: server.CrdtScopeRole.readOnly,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
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 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 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.',
|
||||
'when the role is resolved for that scope, '
|
||||
'then the readOnly role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
|
|
@ -142,106 +233,39 @@ void main() {
|
|||
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 membership is checked for a scope the user does not belong to, '
|
||||
'then the user is not a member.',
|
||||
() async {
|
||||
final session = sessionBuilder.build();
|
||||
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: otherScopeUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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 the role is resolved for the other user's scope, "
|
||||
'then the correct role is returned.',
|
||||
() async {
|
||||
expect(
|
||||
await CrdtScopeMembership.roleOf(
|
||||
session,
|
||||
userUuid: otherUserUuid,
|
||||
scopeUuid: otherScopeUuid,
|
||||
),
|
||||
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 a stored membership row has no role, '
|
||||
'then resolving grants fails instead of treating it as a grant.',
|
||||
'when membership is checked for that scope, '
|
||||
'then the user is not a member.',
|
||||
() 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<StateError>().having(
|
||||
(error) => error.message,
|
||||
'message',
|
||||
'crdt_scope_members.role must not be null.',
|
||||
),
|
||||
expect(
|
||||
await CrdtScopeMembership.isMember(
|
||||
session,
|
||||
userUuid: userUuid,
|
||||
scopeUuid: sharedScopeUuid,
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue