implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling.

This commit is contained in:
2026-06-02 21:42:08 +02:00
parent b6d06dd3b4
commit baa26a6e79
33 changed files with 2453 additions and 29 deletions
+42
View File
@@ -0,0 +1,42 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/files_sharing/ocs_path.dart';
import 'package:marianum_mobile/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
void main() {
group('ocsPathFor', () {
test('files root maps to a single slash', () {
expect(ocsPathFor(''), '/');
expect(ocsPathFor('/'), '/');
});
test('a file path gets a leading slash', () {
expect(ocsPathFor('Documents/x.pdf'), '/Documents/x.pdf');
});
test('a folder loses its trailing slash', () {
expect(ocsPathFor('Documents/'), '/Documents');
expect(ocsPathFor('Shared/a/'), '/Shared/a');
});
test('collapses leading/trailing slashes', () {
expect(ocsPathFor('/Shared/a/'), '/Shared/a');
});
});
group('ocsPathOf', () {
test('derives from a CacheableFile path', () {
final folder = CacheableFile(
path: 'Documents/',
isDirectory: true,
name: 'Documents',
);
final file = CacheableFile(
path: 'Documents/report.pdf',
isDirectory: false,
name: 'report.pdf',
);
expect(ocsPathOf(folder), '/Documents');
expect(ocsPathOf(file), '/Documents/report.pdf');
});
});
}
@@ -0,0 +1,68 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/files_sharing/queries/share/share.dart';
void main() {
group('Share.fromJson', () {
test('parses a public link share (id/share_type as strings)', () {
final share = Share.fromJson({
'id': '42',
'share_type': '3',
'permissions': 1,
'path': '/Documents/x.pdf',
'item_type': 'file',
'url': 'https://cloud.example/s/abc',
'token': 'abc',
'expiration': '2026-07-01 00:00:00',
});
expect(share.id, 42);
expect(share.shareType, kShareTypePublicLink);
expect(share.isPublicLink, isTrue);
expect(share.url, 'https://cloud.example/s/abc');
expect(share.expiration, '2026-07-01 00:00:00');
});
test('parses a user share with display name', () {
final share = Share.fromJson({
'id': 7,
'share_type': 0,
'permissions': 19,
'share_with': 'jdoe',
'share_with_displayname': 'John Doe',
'item_type': 'folder',
});
expect(share.shareType, kShareTypeUser);
expect(share.isFolder, isTrue);
expect(share.displayTitle, 'John Doe');
});
test('group share falls back to share_with when no display name', () {
final share = Share.fromJson({
'id': 8,
'share_type': 1,
'permissions': 1,
'share_with': 'students',
});
expect(share.isGroup, isTrue);
expect(share.displayTitle, 'students');
});
test('missing optional fields become null', () {
final share = Share.fromJson({
'id': 1,
'share_type': 0,
'permissions': 1,
});
expect(share.url, isNull);
expect(share.expiration, isNull);
expect(share.shareWithDisplayname, isNull);
// empty strings are treated as absent
final withEmpties = Share.fromJson({
'id': 1,
'share_type': 0,
'permissions': 1,
'url': '',
});
expect(withEmpties.url, isNull);
});
});
}
@@ -0,0 +1,80 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/files_sharing/share_permissions.dart';
void main() {
group('permissionsFor', () {
test('readOnly is just the read bit', () {
expect(permissionsFor(SharePreset.readOnly), kPermissionRead);
});
test('edit on a file is only read+update (no create/delete)', () {
expect(
permissionsFor(SharePreset.edit),
kPermissionRead | kPermissionUpdate,
);
});
test('edit on a folder adds create+delete', () {
expect(
permissionsFor(SharePreset.edit, isFolder: true),
kPermissionRead |
kPermissionUpdate |
kPermissionCreate |
kPermissionDelete,
);
});
test('edit adds the share bit when reshare is allowed', () {
final mask = permissionsFor(SharePreset.edit, allowReshare: true);
expect(hasPermission(mask, kPermissionShare), isTrue);
expect(hasPermission(mask, kPermissionUpdate), isTrue);
});
test('fileDrop is create-only (upload)', () {
expect(permissionsFor(SharePreset.fileDrop), kPermissionCreate);
expect(
hasPermission(permissionsFor(SharePreset.fileDrop), kPermissionRead),
isFalse,
);
});
});
group('presetFromBitmask', () {
test('round-trips readOnly and fileDrop', () {
expect(
presetFromBitmask(permissionsFor(SharePreset.readOnly)),
SharePreset.readOnly,
);
expect(
presetFromBitmask(permissionsFor(SharePreset.fileDrop)),
SharePreset.fileDrop,
);
});
test('edit is recognised regardless of the reshare bit', () {
expect(
presetFromBitmask(permissionsFor(SharePreset.edit)),
SharePreset.edit,
);
expect(
presetFromBitmask(permissionsFor(SharePreset.edit, allowReshare: true)),
SharePreset.edit,
);
});
test('returns null for an unmatched mask', () {
// share-only (16) with no read bit matches nothing.
expect(presetFromBitmask(kPermissionShare), isNull);
expect(presetFromBitmask(0), isNull);
});
});
group('hasPermission', () {
test('detects individual bits', () {
const mask = kPermissionRead | kPermissionUpdate;
expect(hasPermission(mask, kPermissionRead), isTrue);
expect(hasPermission(mask, kPermissionUpdate), isTrue);
expect(hasPermission(mask, kPermissionDelete), isFalse);
});
});
}