JJ_MKVMaker/test/sftp_stream_test.dart

130 lines
6.1 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/core/playlist.dart' show PlaylistMode;
import 'package:jj_mkvmaker/core/remote_stream.dart';
import 'package:jj_mkvmaker/core/sftp.dart';
import 'package:jj_mkvmaker/core/vfs.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:path/path.dart' as p;
import 'support/sftp_server.dart';
/// sftp_next: SFTP 동영상을 받지 않고 이 기기 안의 다리 (127.0.0.1) 로 스트리밍 - 같은 폴더의 다음 동영상 · 그 자막.
/// 실제 서버 시험은 tool/sftp_test_server.py (paramiko) - 파이썬이 없으면 건너뛴다 (sftp_test 와 같음)
void main() {
final hasServer = sftpTestServerAvailable;
late Directory tmp;
late Directory root;
SftpTestServer? server;
setUp(() async {
if (!hasServer) return;
tmp = Directory.systemTemp.createTempSync('jj_sftpnext_');
root = Directory(p.join(tmp.path, 'root'))..createSync();
final s = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hostkey'));
server = s;
SftpRegistry.configure([
SftpServer(id: 's1', name: 'test', host: '127.0.0.1', port: s.port, user: 'jj', password: 'pw', hostKeyFingerprint: s.fingerprint),
]);
Directory(p.join(root.path, '영상')).createSync();
File(p.join(root.path, '영상', '1화.mkv')).writeAsStringSync('0123456789');
File(p.join(root.path, '영상', '2화.mkv')).writeAsStringSync('abcdefghij');
File(p.join(root.path, '영상', '2화.ko.srt')).writeAsStringSync('1\n00:00:01,000 --> 00:00:02,000\n둘째\n');
File(p.join(root.path, '영상', '메모.txt')).writeAsStringSync('t');
});
tearDown(() async {
if (!hasServer) return;
await RemoteStreamBridge.close();
SftpRegistry.configure(const []);
final s = server;
server = null;
await s?.stop();
for (var i = 0;; i++) {
try {
if (tmp.existsSync()) tmp.deleteSync(recursive: true);
break;
} on FileSystemException {
if (i > 30) rethrow;
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}
});
Future<(int, Map<String, String>, String)> get(String url, {String method = 'GET', String? range}) async {
final http = HttpClient();
try {
final req = await http.openUrl(method, Uri.parse(url));
if (range != null) req.headers.set('Range', range);
final res = await req.close();
final body = await res.transform(utf8.decoder).join();
final h = <String, String>{};
res.headers.forEach((k, v) => h[k] = v.join(','));
return (res.statusCode, h, body);
} finally {
http.close(force: true);
}
}
test('부분 읽기: 시작 · 길이', () async {
final client = DavPath.parse('sftp://s1/').client as SftpRemote;
Future<String> read(int off, int? len) async =>
utf8.decode(await (await client.openReadFrom('영상/1화.mkv', offset: off, length: len)).expand((x) => x).toList());
expect(await read(0, null), '0123456789');
expect(await read(3, 4), '3456');
expect(await read(8, null), '89');
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음');
test('같은 폴더 동영상 목록 · 다리로 스트리밍 (Range · 건너뛰기) · 다음 파일 · 그 자막', () async {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.playlistMode = PlaylistMode.folder;
final plan = await c.preparePlayback(['sftp://s1/영상/1화.mkv']);
expect(plan!.$1, ['sftp://s1/영상/1화.mkv', 'sftp://s1/영상/2화.mkv'], reason: '같은 폴더의 동영상만 차례로');
final bridge = await RemoteStreamBridge.get();
final u1 = bridge.url('sftp://s1/영상/1화.mkv');
final u2 = bridge.url('sftp://s1/영상/2화.mkv');
expect(u1, startsWith('http://127.0.0.1:'));
expect(u1, endsWith('/${Uri.encodeComponent('1화.mkv')}'), reason: 'mpv 가 확장자로 형식을 알게');
expect(u1, isNot(equals(u2)));
expect(bridge.url('sftp://s1/영상/1화.mkv'), u1, reason: '같은 경로는 같은 주소');
expect(u1, isNot(contains('pw')), reason: '주소에 비밀번호 없음');
var (code, h, body) = await get(u1);
expect((code, body), (200, '0123456789'));
expect(h['accept-ranges'], 'bytes');
expect(h['content-type'], 'video/x-matroska');
(code, h, body) = await get(u1, range: 'bytes=4-6');
expect((code, body, h['content-range']), (206, '456', 'bytes 4-6/10'));
(code, h, body) = await get(u1, range: 'bytes=7-');
expect((code, body), (206, '789'));
(code, h, body) = await get(u1, range: 'bytes=-2');
expect((code, body), (206, '89'));
(code, h, _) = await get(u1, range: 'bytes=20-');
expect(code, 416);
(code, h, body) = await get(u1, method: 'HEAD');
expect((code, h['content-length']), (200, '10'));
(code, _, body) = await get(u2);
expect(body, 'abcdefghij', reason: '다음 파일도 받지 않고 바로');
// 비밀값이 틀린 주소 · 등록하지 않은 번호 · 쓰기는 안 됨
(code, _, _) = await get(u1.replaceFirst(RegExp(r'127\.0\.0\.1:\d+/[^/]+/'), '${Uri.parse(u1).authority}/xxx/'));
expect(code, 404);
(code, _, _) = await get(u1.replaceFirst('/0/', '/99/'));
expect(code, 404);
(code, _, _) = await get(u1, method: 'PUT');
expect(code, 405);
expect(File(p.join(root.path, '영상', '1화.mkv')).readAsStringSync(), '0123456789');
// 다음 파일로 넘어가면 플레이어가 그 파일의 자막을 찾는다 (원격 자막만 임시로 받음)
final subs = await c.externalSubtitlesFor('sftp://s1/영상/2화.mkv');
expect(subs.map(p.basename), ['2화.ko.srt']);
expect(File(subs.single).readAsStringSync(), contains('둘째'));
expect(await c.externalSubtitlesFor('sftp://s1/영상/1화.mkv'), isEmpty);
}, skip: hasServer ? false : 'SFTP 시험 서버 (paramiko) 없음');
}