A todo and personal organisation app
1import 'dart:io';
2import 'package:test/test.dart';
3import 'package:shelf/shelf.dart';
4import 'package:toadist_server/routes/files_api.dart';
5import 'package:toadist_server/middleware/auth_middleware.dart';
6import 'package:toadist_server/services/storage_service.dart';
7import 'package:toadist_server/services/database_service.dart';
8
9/// In-memory fake storage that avoids real database connections.
10/// Overrides only the file-ownership methods needed for security tests.
11class FakeStorageService extends StorageService {
12 final Map<String, String> _fileOwners = {};
13
14 FakeStorageService()
15 : super(
16 db: DatabaseService(
17 config: const DatabaseConfig(
18 host: 'localhost',
19 port: 5432,
20 database: 'fake',
21 username: 'fake',
22 password: 'fake')));
23
24 @override
25 Future<void> storeFileOwner(
26 String uniqueFilename, String userId, String originalFilename) async {
27 _fileOwners[uniqueFilename] = userId;
28 }
29
30 @override
31 Future<String?> getFileOwner(String uniqueFilename) async {
32 return _fileOwners[uniqueFilename];
33 }
34}
35
36/// Build a GET request for the given file name, optionally with a userId in
37/// the auth context (as the auth middleware would populate it).
38Request _buildDownloadRequest(String filename, {String? userId}) {
39 final context = userId != null
40 ? {userContextKey: <String, dynamic>{'userId': userId, 'username': 'testuser'}}
41 : <String, Object>{};
42 return Request(
43 'GET',
44 Uri.parse('http://localhost/$filename'),
45 context: context,
46 );
47}
48
49void main() {
50 group('FilesApi – download security', () {
51 late FakeStorageService fakeStorage;
52 late FilesApi filesApi;
53
54 setUp(() {
55 fakeStorage = FakeStorageService();
56 filesApi = FilesApi(storage: fakeStorage);
57 });
58
59 test('unauthenticated request returns 401', () async {
60 final request = _buildDownloadRequest('secret.txt');
61 final response = await filesApi.router(request);
62 expect(response.statusCode, 401);
63 });
64
65 test('file owned by another user returns 403', () async {
66 await fakeStorage.storeFileOwner(
67 'abc123.txt', 'owner-user-id', 'original.txt');
68
69 final request =
70 _buildDownloadRequest('abc123.txt', userId: 'attacker-user-id');
71 final response = await filesApi.router(request);
72 expect(response.statusCode, 403);
73 });
74
75 test('unknown file returns 404 for authenticated user', () async {
76 // No entry in fakeStorage for this filename
77 final request = _buildDownloadRequest(
78 'no-such-file-${DateTime.now().millisecondsSinceEpoch}.txt',
79 userId: 'some-user-id');
80 final response = await filesApi.router(request);
81 expect(response.statusCode, 404);
82 });
83
84 test('file owner receives file contents', () async {
85 const ownerId = 'owner-user-id';
86 const uniqueFilename = 'owner-test-unit.txt';
87 const content = 'secret file content';
88
89 // Create uploads dir relative to test CWD and write a test file
90 final uploadsDir = Directory('uploads');
91 if (!await uploadsDir.exists()) {
92 await uploadsDir.create();
93 }
94 final testFile = File('uploads/$uniqueFilename');
95 await testFile.writeAsString(content);
96
97 try {
98 await fakeStorage.storeFileOwner(
99 uniqueFilename, ownerId, 'original.txt');
100
101 final request =
102 _buildDownloadRequest(uniqueFilename, userId: ownerId);
103 final response = await filesApi.router(request);
104
105 expect(response.statusCode, 200);
106 expect(await response.readAsString(), content);
107 } finally {
108 if (await testFile.exists()) await testFile.delete();
109 }
110 });
111 });
112}