CH-J SM MobileSSH کے ذریعے سرور کا انتظام
مینو
شائع شدہ سورس

CH-J SM Mobile

ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔

سورس ZIP ڈاؤن لوڈ کریں
CH-J / SM Mobile 175 فائلیں
sm-mobile-20261016T185235Z-8e93ecc1b59a شامل کیا گیا: · بلڈ کیا گیا:
sftp_editor_test.dart شامل کیا گیا: تاریخ درج نہیں 6.9 KB · 203 لائنیں
کوڈ فائل ڈاؤن لوڈ کریں
1import 'dart:convert';
2import 'dart:io';
3import 'dart:typed_data';
4import 'package:flutter_test/flutter_test.dart';
5import 'package:sm_mobile/core/failure.dart';
6import 'package:sm_mobile/models/server_profile.dart';
7import 'package:sm_mobile/services/editor/sources.dart';
8import 'package:sm_mobile/services/editor/text_codec.dart';
9import 'package:sm_mobile/services/sftp_service.dart';
10import 'package:sm_mobile/services/ssh_service.dart';
11import 'package:sm_mobile/services/vault.dart';
12import '../support.dart';
14/// Editing remote files over a real, loopback-only OpenSSH server
15/// (tool/test_openssh.py). The remote directory is local, so the tests can
16/// change files "behind the editor's back" and inspect inodes.
17void 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 size
101 file.setLastModifiedSync(stamp); // same time
102 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; the
155 // 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_print
193 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 );
SHA-256db89c272755cda343fa13480170c76ebf0368b6d633d31924288a740ca230eda

CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔

آرکائیو کا SHA-25646e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
فائلیں