233 lines
13 KiB
Dart
233 lines
13 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:jj_mkvmaker/core/app_update.dart';
|
|
import 'package:jj_mkvmaker/platform/windows/github_updater.dart';
|
|
import 'package:path/path.dart' as p;
|
|
|
|
// 화면 테스트는 update_ui_test.dart (화면 테스트가 같은 파일에 있으면 실제 네트워크 연결이 막힘)
|
|
|
|
Map<String, dynamic> _release(String tag, {String? digest, String body = '', bool draft = false, String? url}) => {
|
|
'tag_name': tag,
|
|
'name': 'JJ_MKVMaker $tag',
|
|
'body': body,
|
|
'draft': draft,
|
|
'prerelease': false,
|
|
'html_url': 'https://github.com/$updateRepo/releases/tag/$tag',
|
|
'assets': [
|
|
{
|
|
'name': 'JJ_MKVMaker_${tag}_win64.zip',
|
|
'size': 1234,
|
|
'browser_download_url': url ?? 'https://example.com/x.zip',
|
|
'digest': ?digest,
|
|
},
|
|
],
|
|
};
|
|
|
|
void main() {
|
|
group('버전 · Release 해석', () {
|
|
test('버전 비교', () {
|
|
expect(compareVersions('1.0.1', '1.0.0'), greaterThan(0));
|
|
expect(compareVersions('v1.2', '1.2.0'), 0);
|
|
expect(compareVersions('1.10.0', '1.9.9'), greaterThan(0));
|
|
expect(compareVersions('1.0.0+5', 'v1.0.1'), lessThan(0));
|
|
});
|
|
test('버전: 년.월.일_순번', () {
|
|
expect(parseVersion('v2026.09.30_001'), [2026, 9, 30, 1]);
|
|
expect(formatVersion('2026.9.30+1'), '2026.09.30_001'); // pubspec 의 표기 → 보이는 글
|
|
expect(formatVersion('v2026.09.30_012'), '2026.09.30_012');
|
|
expect(formatVersion('2026.10.1+3'), '2026.10.01_003');
|
|
expect(formatVersion('2026.10.2+261002001'), '2026.10.02_001'); // Android versionCode
|
|
expect(formatVersion('v1.2.3'), '1.2.3'); // 예전 방식은 그대로
|
|
// 같은 날: 순번이 크면 최신. 날이 바뀌면 001 이어도 최신.
|
|
expect(compareVersions('2026.09.30_002', '2026.9.30+1'), greaterThan(0));
|
|
expect(compareVersions('v2026.09.30_001', '2026.9.30+1'), 0);
|
|
expect(compareVersions('2026.10.01_001', '2026.09.30_015'), greaterThan(0));
|
|
expect(compareVersions('2027.01.01_001', '2026.12.31_009'), greaterThan(0));
|
|
// 예전 1.x 에서 쓰던 사람도 새 방식을 업데이트로 받는다
|
|
expect(compareVersions('v2026.09.30_001', '1.1.1'), greaterThan(0));
|
|
final r = parseLatestRelease(_release('v2026.09.30_002'))!;
|
|
expect(r.version, '2026.09.30_002');
|
|
expect(r.isNewerThan('2026.09.30_001'), isTrue);
|
|
expect(r.isNewerThan('2026.09.30_002'), isFalse);
|
|
});
|
|
test('releases/latest 해석', () {
|
|
final r = parseLatestRelease(_release('v1.2.0', digest: 'sha256:${'A' * 64}'))!;
|
|
expect([r.version, r.tag, r.zipName, r.sha256], ['1.2.0', 'v1.2.0', 'JJ_MKVMaker_v1.2.0_win64.zip', 'a' * 64]);
|
|
expect(r.isNewerThan('1.0.1'), isTrue);
|
|
// digest 없으면 설명의 SHA256
|
|
final b = parseLatestRelease(_release('v1.2.0', body: '| zip | `${'B' * 64}` |'))!;
|
|
expect(b.sha256, 'b' * 64);
|
|
expect(parseLatestRelease(_release('v9.0.0', draft: true)), isNull);
|
|
});
|
|
|
|
test('Android: APK 파일 · 그 파일 줄의 SHA256', () {
|
|
final j = _release('v2026.10.03_001',
|
|
body: '| `JJ_MKVMaker_v2026.10.03_001_win64.zip` | `${'C' * 64}` |\n'
|
|
'| `JJ_MKVMaker_v2026.10.03_001_android_arm64.apk` | `${'D' * 64}` |');
|
|
(j['assets'] as List).add({
|
|
'name': 'JJ_MKVMaker_v2026.10.03_001_android_arm64.apk',
|
|
'size': 92091507,
|
|
'browser_download_url': 'https://example.com/x.apk',
|
|
});
|
|
final a = parseLatestRelease(j, assetPattern: androidAssetPattern)!;
|
|
expect([a.zipName, a.zipUrl, a.zipSize, a.sha256],
|
|
['JJ_MKVMaker_v2026.10.03_001_android_arm64.apk', 'https://example.com/x.apk', 92091507, 'd' * 64]);
|
|
// Windows 는 그대로 zip
|
|
expect(parseLatestRelease(j)!.sha256, 'c' * 64);
|
|
// APK 가 없는 판
|
|
expect(parseLatestRelease(_release('v1.0.0'), assetPattern: androidAssetPattern)!.zipUrl, isNull);
|
|
});
|
|
test('하루 한 번', () {
|
|
final now = DateTime(2026, 10, 1, 12);
|
|
expect(updateCheckDue('', now), isTrue);
|
|
expect(updateCheckDue(DateTime(2026, 10, 1, 9).toIso8601String(), now), isFalse);
|
|
expect(updateCheckDue(DateTime(2026, 9, 30, 9).toIso8601String(), now), isTrue);
|
|
// 방금 확인했어도 다른 버전이 한 확인이면 다시 (같은 설정 파일을 쓰는 새 버전 개발 빌드 등)
|
|
final recent = DateTime(2026, 10, 1, 9).toIso8601String();
|
|
expect(updateCheckDue(recent, now, checkedBy: '2026.10.04_012', current: '2026.10.01_003'), isTrue);
|
|
expect(updateCheckDue(recent, now, checkedBy: '', current: '2026.10.01_003'), isTrue); // 예전 판이 남긴 기록
|
|
expect(updateCheckDue(recent, now, checkedBy: '2026.10.01_003', current: '2026.10.01_003'), isFalse);
|
|
});
|
|
});
|
|
|
|
test('실제 GitHub: v1.0.0 에서 최신 찾기 · 받기 · 검증 (JJ_NET_TESTS=1)', () async {
|
|
if (Platform.environment['JJ_NET_TESTS'] != '1') return markTestSkipped('인터넷 테스트 꺼짐');
|
|
final tmp = Directory.systemTemp.createTempSync('jj_real_upd_');
|
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
|
final up = GitHubUpdater(appDir: tmp.path, versionOverride: '1.0.0');
|
|
final r = (await up.latest())!;
|
|
expect(r.isNewerThan('1.0.0'), isTrue);
|
|
expect(r.sha256, hasLength(64));
|
|
final dir = await up.download(r, (_) {});
|
|
expect(File(p.join(dir, 'jj_mkvmaker.exe')).existsSync(), isTrue);
|
|
expect(File(p.join(dir, 'ffmpeg', 'ffmpeg.exe')).existsSync(), isTrue);
|
|
}, timeout: const Timeout(Duration(minutes: 10)));
|
|
|
|
group('GitHubUpdater (가짜 GitHub 서버 · 실제 스크립트)', () {
|
|
late HttpServer server;
|
|
late Directory tmp;
|
|
late List<int> zipBytes;
|
|
var digest = '';
|
|
|
|
setUpAll(() async {
|
|
tmp = Directory.systemTemp.createTempSync('jj_upd_');
|
|
// 새 버전 zip: JJ_MKVMaker\jj_mkvmaker.exe · new.txt · data\app.so
|
|
final src = Directory(p.join(tmp.path, 'pkg', 'JJ_MKVMaker'))..createSync(recursive: true);
|
|
File(p.join(src.path, 'jj_mkvmaker.exe')).writeAsStringSync('NEW EXE');
|
|
File(p.join(src.path, 'new.txt')).writeAsStringSync('new file');
|
|
Directory(p.join(src.path, 'data')).createSync();
|
|
File(p.join(src.path, 'data', 'app.so')).writeAsStringSync('NEW DATA');
|
|
final zip = p.join(tmp.path, 'u.zip');
|
|
final r = await Process.run('tar', ['-a', '-cf', zip, '-C', p.join(tmp.path, 'pkg'), 'JJ_MKVMaker']);
|
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
|
zipBytes = File(zip).readAsBytesSync();
|
|
digest = sha256.convert(zipBytes).toString();
|
|
|
|
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
server.listen((req) async {
|
|
final base = 'http://127.0.0.1:${server.port}';
|
|
if (req.uri.path == '/repos/$updateRepo/releases/latest') {
|
|
req.response.headers.contentType = ContentType.json;
|
|
final bad = req.uri.queryParameters['bad'] == '1';
|
|
req.response.write(jsonEncode(_release('v1.9.0',
|
|
digest: 'sha256:${bad ? '0' * 64 : digest}', url: '$base/dl/u.zip')));
|
|
} else if (req.uri.path == '/dl/u.zip') {
|
|
req.response
|
|
..contentLength = zipBytes.length
|
|
..add(zipBytes);
|
|
} else {
|
|
req.response.statusCode = 404;
|
|
}
|
|
await req.response.close();
|
|
});
|
|
});
|
|
tearDownAll(() async {
|
|
await server.close(force: true);
|
|
tmp.deleteSync(recursive: true);
|
|
});
|
|
|
|
test('최신 확인 → 받기 · SHA256 확인 · 압축 풀기', () async {
|
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, versionOverride: '1.0.1', tempRoot: tmp.path);
|
|
final r = (await up.latest())!;
|
|
expect(r.isNewerThan(await up.currentVersion()), isTrue);
|
|
final progress = <double>[];
|
|
final dir = await up.download(r, progress.add);
|
|
expect(File(p.join(dir, 'jj_mkvmaker.exe')).readAsStringSync(), 'NEW EXE');
|
|
expect(progress.last, 1);
|
|
});
|
|
|
|
test('SHA256 이 다르면 설치 거부', () async {
|
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: tmp.path, tempRoot: tmp.path);
|
|
final good = (await up.latest())!;
|
|
final bad = ReleaseInfo(
|
|
version: good.version, tag: good.tag, name: '', notes: '', pageUrl: '',
|
|
zipUrl: good.zipUrl, zipName: good.zipName, sha256: '0' * 64);
|
|
await expectLater(up.download(bad, (_) {}), throwsA(predicate((e) => '$e'.contains('SHA256'))));
|
|
});
|
|
|
|
test('관리자 지시 (10/20): 시험 길 JJ_UPDATE_LOCAL_ZIP - 시험용 데이터 폴더일 때만 · sha256 이 꼭 있고 다르면 거절', () async {
|
|
final zip = p.join(tmp.path, 'JJ_MKVMaker_v2026.10.20_001_win64.zip');
|
|
File(zip).writeAsBytesSync(zipBytes);
|
|
final env = {'JJ_UPDATE_LOCAL_ZIP': '$zip;${digest.toUpperCase()}'};
|
|
// 사용자 판 (따로 데이터 없음) 은 무시 - GitHub 그대로
|
|
expect(LocalUpdateZip.fromEnvironment(env, null), isNull);
|
|
expect(LocalUpdateZip.fromEnvironment(const {}, 'D:/data'), isNull);
|
|
// sha256 이 없거나 꼴이 틀리면 거절
|
|
for (final v in [zip, '$zip;', '$zip;abc']) {
|
|
expect(() => LocalUpdateZip.fromEnvironment({'JJ_UPDATE_LOCAL_ZIP': v}, 'D:/data'),
|
|
throwsA(predicate((e) => '$e'.contains('sha256'))), reason: v);
|
|
}
|
|
final local = LocalUpdateZip.fromEnvironment(env, 'D:/data')!;
|
|
expect((local.path, local.sha256, local.version), (zip, digest, '2026.10.20_001'));
|
|
// GitHub 를 부르지 않음 (닿지 않는 주소) - 확인 · 목록 · 받기 모두 이 PC 의 zip
|
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:1', appDir: tmp.path, versionOverride: '2026.10.18_001', tempRoot: tmp.path, local: () => local);
|
|
final r = (await up.latest())!;
|
|
expect(r.name, contains('시험 길'), reason: '화면에 시험 길이라고');
|
|
expect(r.notes, contains('시험 길'));
|
|
expect(r.isNewerThan(await up.currentVersion()), isTrue);
|
|
expect((await up.releases()).single.version, '2026.10.20_001');
|
|
final dir = await up.download(r, (_) {});
|
|
expect(File(p.join(dir, 'jj_mkvmaker.exe')).readAsStringSync(), 'NEW EXE');
|
|
expect(File(zip).existsSync(), isTrue, reason: '원래 zip 은 그대로 (복사해서 씀)');
|
|
// sha256 이 다르면 거절
|
|
final bad = GitHubUpdater(apiBase: 'http://127.0.0.1:1', appDir: tmp.path, tempRoot: tmp.path, local: () => LocalUpdateZip(zip, '0' * 64));
|
|
await expectLater(bad.download((await bad.latest())!, (_) {}), throwsA(predicate((e) => '$e'.contains('SHA256'))));
|
|
// 시험 길이 아닌데 file: 주소가 오면 쓰지 않음
|
|
final none = GitHubUpdater(apiBase: 'http://127.0.0.1:1', appDir: tmp.path, tempRoot: tmp.path, local: () => null);
|
|
await expectLater(none.download(r, (_) {}), throwsA(predicate((e) => '$e'.contains('시험 길'))));
|
|
});
|
|
|
|
test('업데이트 스크립트: 프로그램 파일만 교체, 받은 파일 · 모델 보존', () async {
|
|
if (!Platform.isWindows) return markTestSkipped('Windows 전용');
|
|
final app = Directory(p.join(tmp.path, 'installed'))..createSync();
|
|
File(p.join(app.path, 'jj_mkvmaker.exe')).writeAsStringSync('OLD EXE');
|
|
File(p.join(app.path, 'old_only.txt')).writeAsStringSync('keep me');
|
|
Directory(p.join(app.path, 'jj_yt-dlp')).createSync();
|
|
File(p.join(app.path, 'jj_yt-dlp', 'video.mp4')).writeAsStringSync('my download');
|
|
Directory(p.join(app.path, 'models')).createSync();
|
|
File(p.join(app.path, 'models', 'm.bin')).writeAsStringSync('model');
|
|
|
|
final up = GitHubUpdater(apiBase: 'http://127.0.0.1:${server.port}', appDir: app.path, tempRoot: tmp.path);
|
|
final dir = await up.download((await up.latest())!, (_) {});
|
|
// 이미 끝난 프로세스를 "앱" 으로 두고 실행 (바로 교체 진행)
|
|
final dummy = await Process.start('cmd', ['/c', 'exit']);
|
|
await dummy.exitCode;
|
|
final proc = await up.runInstallScript(dir, waitPid: dummy.pid, restart: false);
|
|
expect(await proc.exitCode, 0);
|
|
|
|
expect(File(p.join(app.path, 'jj_mkvmaker.exe')).readAsStringSync(), 'NEW EXE');
|
|
expect(File(p.join(app.path, 'new.txt')).existsSync(), isTrue);
|
|
expect(File(p.join(app.path, 'data', 'app.so')).readAsStringSync(), 'NEW DATA');
|
|
expect(File(p.join(app.path, 'old_only.txt')).readAsStringSync(), 'keep me');
|
|
expect(File(p.join(app.path, 'jj_yt-dlp', 'video.mp4')).readAsStringSync(), 'my download');
|
|
expect(File(p.join(app.path, 'models', 'm.bin')).readAsStringSync(), 'model');
|
|
// 기록 · 스크립트는 스크립트 옆 (실제로는 %TEMP%, 시험은 시험 폴더 - 시험이 %TEMP% 에 남기지 않음)
|
|
expect(File(p.join(tmp.path, 'jj_mkvmaker_update.log')).existsSync(), isTrue);
|
|
expect(File(p.join(tmp.path, 'jj_mkvmaker_update.ps1')).existsSync(), isTrue);
|
|
}, timeout: const Timeout(Duration(minutes: 2)));
|
|
});
|
|
}
|