Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J SM Mobile
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / SM Mobile
175 Dateien
sm-mobile-20261016T185235Z-8e93ecc1b59a
Hinzugefügt: · Erstellt:
1
import 'dart:convert';2
import 'dart:io';3
import 'dart:typed_data';4
import 'package:flutter_test/flutter_test.dart';5
import 'package:sm_mobile/core/failure.dart';6
import 'package:sm_mobile/models/server_profile.dart';7
import 'package:sm_mobile/services/editor/sources.dart';8
import 'package:sm_mobile/services/editor/text_codec.dart';9
import 'package:sm_mobile/services/sftp_service.dart';10
import 'package:sm_mobile/services/ssh_service.dart';11
import 'package:sm_mobile/services/vault.dart';12
import '../support.dart';14
/// Editing remote files over a real, loopback-only OpenSSH server15
/// (tool/test_openssh.py). The remote directory is local, so the tests can16
/// change files "behind the editor's back" and inspect inodes.17
void main() {18
TestWidgetsFlutterBinding.ensureInitialized();19
final enabled = Platform.environment['SM_OPENSSH_TEST'] == '1';20
late Vault vault;21
late SshService ssh;22
late SftpService sftp;23
late String root;24
setUpAll(() async {25
if (!enabled) return;26
vault = Vault(MemoryStore());27
await vault.create('sftp editor test password');28
ssh = SshService(vault);29
final profile = ServerProfile(30
name: 'Isolated OpenSSH',31
host: '127.0.0.1',32
username: Platform.environment['USER']!,33
port: 22229,34
auth: AuthMethod.privateKey,35
);36
await ssh.connect(37
profile,38
credential: await File(39
Platform.environment['SM_TEST_KEY']!,40
).readAsString(),41
passphrase: 'isolated-test-passphrase',42
prompt: (_, _, _) async => true,43
);44
sftp = SftpService(await ssh.client.sftp());45
root = await Directory(46
Platform.environment['SM_TEST_REMOTE_DIR']!,47
).resolveSymbolicLinks();48
root = '$root/editor-${newId()}';49
await Directory(root).create();50
});51
tearDownAll(() {52
if (enabled) {53
ssh.dispose();54
vault.lock();55
}56
});58
RemoteSource source(String path) => RemoteSource(59
service: () async => sftp,60
profileId: 'p',61
profileName: 'test',62
path: path,63
);65
List<String> leftovers() => Directory(root)66
.listSync()67
.map((e) => e.path.split('/').last)68
.where((n) => n.contains('.sm-'))69
.toList();71
test(72
'open, edit and save keeps permissions; no temporary files remain',73
() async {74
final file = File('$root/nginx.conf')75
..writeAsStringSync('server {\r\n listen 80;\r\n}\r\n');76
await Process.run('chmod', ['640', file.path]);77
final s = source(file.path);78
final decoded = decodeDocument(await s.load());79
expect(decoded.lineBreaks.dominant, LineBreak.crlf);80
final edited = encodeText(81
withLineBreaks('${decoded.text}# edited\n', LineBreak.crlf),82
decoded.encoding,83
);84
await s.save(edited);85
expect(file.readAsBytesSync(), edited);86
final mode = (await Process.run('stat', ['-f', '%Lp', file.path])).stdout;87
expect(mode.toString().trim(), '640');88
expect(leftovers(), isEmpty);89
},90
skip: !enabled,91
);93
test(94
'a change on the server with equal size and time is a conflict',95
() async {96
final file = File('$root/app.env')..writeAsStringSync('A=1\nB=2\n');97
final s = source(file.path);98
await s.load();99
final stamp = file.lastModifiedSync();100
file.writeAsStringSync('A=9\nB=2\n'); // same size101
file.setLastModifiedSync(stamp); // same time102
await expectLater(103
s.save(Uint8List.fromList(utf8.encode('A=1\nB=3\n'))),104
throwsA(isA<Failure>().having((f) => f.code, 'code', 'remoteChanged')),105
);106
expect(file.readAsStringSync(), 'A=9\nB=2\n', reason: 'untouched');107
await s.save(Uint8List.fromList(utf8.encode('A=1\nB=3\n')), force: true);108
expect(file.readAsStringSync(), 'A=1\nB=3\n');109
file.deleteSync();110
await expectLater(111
s.save(Uint8List.fromList(utf8.encode('x'))),112
throwsA(isA<Failure>().having((f) => f.code, 'code', 'remoteDeleted')),113
);114
expect(leftovers(), isEmpty);115
},116
skip: !enabled,117
);119
test(120
'read-only folder: no atomic save, explicit in-place write keeps inode',121
() async {122
final dir = Directory('$root/locked')..createSync();123
final file = File('${dir.path}/site.yaml')..writeAsStringSync('a: 1\n');124
final inode = (await Process.run('stat', ['-f', '%i', file.path])).stdout;125
await Process.run('chmod', ['555', dir.path]);126
try {127
final s = source(file.path);128
await s.load();129
await expectLater(130
s.save(Uint8List.fromList(utf8.encode('a: 2\n'))),131
throwsA(132
isA<Failure>().having((f) => f.code, 'code', 'atomicNotPossible'),133
),134
);135
expect(file.readAsStringSync(), 'a: 1\n');136
await s.save(Uint8List.fromList(utf8.encode('a: 2\n')), inPlace: true);137
expect(file.readAsStringSync(), 'a: 2\n');138
expect(139
(await Process.run('stat', ['-f', '%i', file.path])).stdout,140
inode,141
);142
} finally {143
await Process.run('chmod', ['755', dir.path]);144
}145
},146
skip: !enabled,147
);149
test('no write permission is reported, the file stays intact', () async {150
final file = File('$root/readonly.txt')..writeAsStringSync('keep\n');151
await Process.run('chmod', ['444', file.path]);152
final s = source(file.path);153
await s.load();154
// The folder is writable, so a rename could replace the file; the155
// missing write permission on the file itself must stop the save.156
for (final inPlace in [false, true]) {157
await expectLater(158
s.save(Uint8List.fromList(utf8.encode('x\n')), inPlace: inPlace),159
throwsA(160
isA<Failure>().having((f) => f.code, 'code', 'permissionDenied'),161
),162
);163
}164
expect(file.readAsStringSync(), 'keep\n');165
}, skip: !enabled);167
test('symbolic links are not followed for editing', () async {168
File('$root/target.txt').writeAsStringSync('t');169
Link('$root/link.txt').createSync('$root/target.txt');170
await expectLater(171
source('$root/link.txt').load(),172
throwsA(isA<Failure>().having((f) => f.code, 'code', 'unsafePath')),173
);174
}, skip: !enabled);176
test(177
'sizes from 1 KB to 10 MB over SFTP',178
() async {179
for (final kb in [1, 100, 1024, 5 * 1024, 10 * 1024]) {180
final bytes = Uint8List.fromList(181
List.generate(kb * 1024, (i) => i % 64 == 63 ? 10 : 97 + i % 26),182
);183
final file = File('$root/size-$kb.txt')..writeAsBytesSync(bytes);184
final s = source(file.path);185
final watch = Stopwatch()..start();186
final loaded = await s.load();187
final read = watch.elapsedMilliseconds;188
expect(loaded, bytes);189
final changed = Uint8List.fromList([65, ...bytes]);190
watch.reset();191
await s.save(changed);192
// ignore: avoid_print193
print(194
'sftp $kb KB: read $read ms, safe save ${watch.elapsedMilliseconds} ms',195
);196
expect(file.readAsBytesSync(), changed);197
}198
expect(leftovers(), isEmpty);199
},200
skip: !enabled,201
timeout: const Timeout(Duration(minutes: 3)),202
);203
}SHA-256
db89c272755cda343fa13480170c76ebf0368b6d633d31924288a740ca230edaCH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655