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:
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/autocomplete/autocomplete_response.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/files_sharing/queries/share/share.dart';
|
||||
|
||||
void main() {
|
||||
group('shareTypeFromSource', () {
|
||||
test('groups map to the group share type', () {
|
||||
expect(shareTypeFromSource('groups'), kShareTypeGroup);
|
||||
// Nextcloud sometimes suffixes the source (e.g. groups-exact).
|
||||
expect(shareTypeFromSource('groups-exact'), kShareTypeGroup);
|
||||
});
|
||||
|
||||
test('users map to the user share type', () {
|
||||
expect(shareTypeFromSource('users'), kShareTypeUser);
|
||||
});
|
||||
|
||||
test('unknown / null sources fall back to user', () {
|
||||
expect(shareTypeFromSource('remotes'), kShareTypeUser);
|
||||
expect(shareTypeFromSource(null), kShareTypeUser);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/webdav/queries/list_files/cacheable_file.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/webdav/queries/list_files/list_files_response.dart';
|
||||
|
||||
void main() {
|
||||
group('ListFilesResponse cache round-trip', () {
|
||||
test('isSharedWithMe survives jsonEncode -> decode -> fromJson', () {
|
||||
final response = ListFilesResponse({
|
||||
CacheableFile(
|
||||
path: 'Shared/',
|
||||
isDirectory: true,
|
||||
name: 'Shared',
|
||||
isSharedWithMe: true,
|
||||
),
|
||||
CacheableFile(
|
||||
path: 'own.txt',
|
||||
isDirectory: false,
|
||||
name: 'own.txt',
|
||||
),
|
||||
});
|
||||
|
||||
// Mirror exactly what RequestCache does: jsonEncode the response, then
|
||||
// read it back through fromJson.
|
||||
final encoded = jsonEncode(response);
|
||||
final decoded = ListFilesResponse.fromJson(
|
||||
jsonDecode(encoded) as Map<String, dynamic>,
|
||||
);
|
||||
|
||||
final shared = decoded.files.firstWhere((f) => f.name == 'Shared');
|
||||
final own = decoded.files.firstWhere((f) => f.name == 'own.txt');
|
||||
|
||||
expect(shared.isSharedWithMe, isTrue);
|
||||
expect(own.isSharedWithMe, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:marianum_mobile/api/marianumcloud/capabilities/nextcloud_sharing_capabilities.dart';
|
||||
|
||||
void main() {
|
||||
group('NextcloudSharingCapabilities.fromFilesSharing', () {
|
||||
test('parses a full files_sharing block', () {
|
||||
final caps = NextcloudSharingCapabilities.fromFilesSharing({
|
||||
'api_enabled': true,
|
||||
'public': {
|
||||
'enabled': true,
|
||||
'multiple_links': true,
|
||||
'upload': true,
|
||||
'password': {'enforced': true},
|
||||
'expire_date': {'enabled': true, 'days': 7, 'enforced': true},
|
||||
},
|
||||
'group': {'enabled': true},
|
||||
'resharing': true,
|
||||
});
|
||||
expect(caps.apiEnabled, isTrue);
|
||||
expect(caps.publicEnabled, isTrue);
|
||||
expect(caps.publicMultipleLinks, isTrue);
|
||||
expect(caps.publicUploadEnabled, isTrue);
|
||||
expect(caps.publicPasswordEnforced, isTrue);
|
||||
expect(caps.publicExpireEnabled, isTrue);
|
||||
expect(caps.publicExpireDays, 7);
|
||||
expect(caps.publicExpireEnforced, isTrue);
|
||||
expect(caps.groupEnabled, isTrue);
|
||||
expect(caps.resharing, isTrue);
|
||||
});
|
||||
|
||||
test('falls back to the legacy group_sharing flag', () {
|
||||
final caps = NextcloudSharingCapabilities.fromFilesSharing({
|
||||
'api_enabled': true,
|
||||
'group_sharing': true,
|
||||
});
|
||||
expect(caps.groupEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('missing fields default to the restrictive value', () {
|
||||
final caps = NextcloudSharingCapabilities.fromFilesSharing({});
|
||||
expect(caps.apiEnabled, isFalse);
|
||||
expect(caps.publicEnabled, isFalse);
|
||||
expect(caps.groupEnabled, isFalse);
|
||||
expect(caps.resharing, isFalse);
|
||||
expect(caps.publicExpireDays, isNull);
|
||||
});
|
||||
|
||||
test('tolerates non-map / wrong-typed nested values', () {
|
||||
final caps = NextcloudSharingCapabilities.fromFilesSharing({
|
||||
'api_enabled': 'yes', // not a bool -> false
|
||||
'public': 'nope', // not a map -> all false
|
||||
});
|
||||
expect(caps.apiEnabled, isFalse);
|
||||
expect(caps.publicEnabled, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user