import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:halkyon_app/halkyon_app.dart'; import 'package:path/path.dart' as p; void main() { late Directory folder; setUp(() async { folder = await Directory.systemTemp.createTemp('file_saver_test'); }); tearDown(() => folder.delete(recursive: true)); test("a saved file is the own user's alone where the platform has modes, " 'set before anything is written', () async { final path = p.join(folder.path, 'halkyon-recovery-codes.txt'); // One there before, readable by everyone. await File(path).writeAsString('old'); if (!Platform.isWindows) { await Process.run('chmod', ['644', path]); } await writePrivately(path, Uint8List.fromList([1, 2, 3])); expect(await File(path).readAsBytes(), [1, 2, 3]); if (Platform.isLinux || Platform.isMacOS) { expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600'); } }, skip: Platform.isWindows ? 'no modes' : false); final hasModes = Platform.isLinux || Platform.isMacOS; test('a saved file replaces the one there without writing into it: ' 'whoever held the old file open, or a link to it, never reads what is ' 'saved (the reading of the fixes)', () async { final path = p.join(folder.path, 'halkyon-recovery-codes.txt'); await File(path).writeAsString('old'); await Process.run('chmod', ['644', path]); // Another name for the same file, as a descriptor opened earlier is. final elsewhere = p.join(folder.path, 'held-open'); await Process.run('ln', [path, elsewhere]); await writePrivately(path, Uint8List.fromList(utf8.encode('secret'))); expect(await File(path).readAsString(), 'secret'); expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600'); expect(await File(elsewhere).readAsString(), 'old'); // Nothing left beside it. expect( folder.listSync().map((entry) => p.basename(entry.path)).toSet(), {'halkyon-recovery-codes.txt', 'held-open'}, ); }, skip: hasModes ? false : 'no modes'); test('never follows a symbolic link where the file is to be saved: ' 'refused, the file it points to untouched', () async { final target = p.join(folder.path, 'someone-elses.txt'); await File(target).writeAsString('theirs'); final path = p.join(folder.path, 'halkyon-recovery-phrase.txt'); await Link(path).create(target); await expectLater( writePrivately(path, Uint8List.fromList(utf8.encode('secret'))), throwsA(isA()), ); expect(await File(target).readAsString(), 'theirs'); expect(FileSystemEntity.isLinkSync(path), isTrue); expect( folder.listSync().map((entry) => p.basename(entry.path)).toSet(), {'someone-elses.txt', 'halkyon-recovery-phrase.txt'}, ); }, skip: hasModes ? false : 'no modes'); test( "refuses a file another user owns, writing nothing: a root's file " 'stands in for one in a shared folder', () async { const rootsFile = '/etc/hostname'; final before = File(rootsFile).readAsStringSync(); await expectLater( writePrivately(rootsFile, Uint8List.fromList([1])), throwsA( isA().having( (error) => error.message, 'message', "Another user's file", ), ), ); expect(File(rootsFile).readAsStringSync(), before); }, skip: !hasModes || !File('/etc/hostname').existsSync() ? 'no such file' : false, ); test('a new file is made 0600 from the start, with nothing left beside ' 'it', () async { final path = p.join(folder.path, 'new.txt'); await writePrivately(path, Uint8List.fromList([4, 5, 6])); expect(await File(path).readAsBytes(), [4, 5, 6]); expect(File(path).statSync().mode & 0x1ff, 0x180, reason: '0600'); expect(folder.listSync(), hasLength(1)); }, skip: hasModes ? false : 'no modes'); test("the system picker's copy in the temporary folder is removed, " 'others left', () async { final copy = File(p.join(folder.path, 'halkyon-recovery-phrase.txt')); final other = File(p.join(folder.path, 'something-else.txt')); await copy.writeAsString('the phrase'); await other.writeAsString('other'); await forgetPickerCopy('halkyon-recovery-phrase.txt', temporary: folder); await forgetPickerCopy('not-there.txt', temporary: folder); expect(copy.existsSync(), isFalse); expect(other.existsSync(), isTrue); }); }