The server's version is stamped like the app's (APP_VERSION) when packaged, so it can compare against the manifest; --upgrade swaps the whole install folder (app and server) and asks for a restart. update.sh/.bat no longer need GTK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
228 lines
8.4 KiB
Dart
228 lines
8.4 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:sync_relay_core/core.dart';
|
|
import 'package:watch_together/src/update/update_controller.dart';
|
|
|
|
// Work dir base: WT_TEST_TMP or the system temp. Staging lives inside
|
|
// the install root, so the result doesn't depend on which filesystem
|
|
// this is (the old /tmp staging broke when /tmp was tmpfs).
|
|
final _tmpBase =
|
|
Platform.environment['WT_TEST_TMP'] ?? Directory.systemTemp.path;
|
|
|
|
Future<Directory> _work() async {
|
|
final base = Directory(_tmpBase);
|
|
await base.create(recursive: true);
|
|
return base.createTemp('wt-up-test');
|
|
}
|
|
|
|
// Linux-layout tests check exec bits and symlinks, which NTFS lacks.
|
|
final _posixOnly = Platform.isWindows ? 'POSIX install layout' : null;
|
|
|
|
bool _isExec(String path) => File(path).statSync().mode & 0x40 != 0;
|
|
|
|
// Builds the release archive exactly like tool/package_linux.sh:
|
|
// bundle copied into <stage>/watch-together-<ver>/, then
|
|
// `tar -czf out -C <stage> watch-together-<ver>`. [zip]: like
|
|
// tool/package_windows.ps1 (`tar -a -c -f out.zip`, bsdtar — Windows only).
|
|
Future<List<int>> _releaseTarball(Directory work,
|
|
{String version = '0.2.0',
|
|
bool windows = false,
|
|
bool bundle = true,
|
|
bool zip = false}) async {
|
|
final stage = Directory('${work.path}/pkg-$version-${windows ? 'win' : 'lin'}');
|
|
final folder = Directory('${stage.path}/watch-together-$version');
|
|
await Directory('${folder.path}/data/flutter_assets').create(recursive: true);
|
|
await File('${folder.path}/data/flutter_assets/fresh.cfg')
|
|
.writeAsString('fresh-config');
|
|
if (windows) {
|
|
await File('${folder.path}/watch_together.exe').writeAsString('binary-v2');
|
|
await File('${folder.path}/flutter_windows.dll').writeAsString('dll-v2');
|
|
} else {
|
|
await File('${folder.path}/watch_together').writeAsString('binary-v2');
|
|
if (bundle) {
|
|
await Directory('${folder.path}/lib').create();
|
|
await File('${folder.path}/lib/libapp.so.1').writeAsString('lib-v2');
|
|
await Link('${folder.path}/lib/libapp.so').create('libapp.so.1');
|
|
}
|
|
await File('${folder.path}/update.sh')
|
|
.writeAsString('#!/bin/sh\nexec ./watch_together --upgrade\n');
|
|
if (!Platform.isWindows) {
|
|
final r = await Process.run('chmod',
|
|
['755', '${folder.path}/watch_together', '${folder.path}/update.sh']);
|
|
expect(r.exitCode, 0);
|
|
}
|
|
}
|
|
final out = File('${work.path}/out-$version.${zip ? 'zip' : 'tar.gz'}');
|
|
final r = await Process.run('tar', [
|
|
...zip ? ['-a', '-c', '-f'] : ['-czf'],
|
|
out.path,
|
|
'-C',
|
|
stage.path,
|
|
'watch-together-$version',
|
|
]);
|
|
expect(r.exitCode, 0, reason: '${r.stderr}');
|
|
return out.readAsBytes();
|
|
}
|
|
|
|
Future<Directory> _fakeRoot(Directory work, {bool windows = false}) async {
|
|
final root = Directory('${work.path}/install');
|
|
await Directory('${root.path}/data').create(recursive: true);
|
|
await File('${root.path}/data/old.cfg').writeAsString('old-config');
|
|
if (windows) {
|
|
await File('${root.path}/watch_together.exe').writeAsString('binary-v1');
|
|
await File('${root.path}/flutter_windows.dll').writeAsString('dll-v1');
|
|
} else {
|
|
await Directory('${root.path}/lib').create();
|
|
await File('${root.path}/lib/libgone.so').writeAsString('lib-v1');
|
|
await File('${root.path}/watch_together').writeAsString('binary-v1');
|
|
if (!Platform.isWindows) {
|
|
await Process.run('chmod', ['755', '${root.path}/watch_together']);
|
|
}
|
|
}
|
|
await File('${root.path}/user-notes.txt').writeAsString('mine');
|
|
return root;
|
|
}
|
|
|
|
// Serves /manifest.json and the tarball. Fields can be overridden per test.
|
|
class _Server {
|
|
late HttpServer http;
|
|
List<int> tarball;
|
|
Map<String, Object?> manifestExtra = {};
|
|
bool brokenManifest = false;
|
|
int tarHits = 0;
|
|
_Server(this.tarball);
|
|
|
|
String get base => 'http://127.0.0.1:${http.port}';
|
|
String get manifestUrl => '$base/manifest.json';
|
|
|
|
Future<void> start() async {
|
|
http = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
|
http.listen((req) async {
|
|
if (req.uri.path == '/manifest.json' && !brokenManifest) {
|
|
req.response.write(jsonEncode({
|
|
'version': '0.2.0',
|
|
'protocol': RelayConstants.protoVersion,
|
|
'url': '$base/watch-together-0.2.0-linux-x64.tar.gz',
|
|
'sha256': sha256.convert(tarball).toString(),
|
|
'notes': 'test build',
|
|
...manifestExtra,
|
|
}));
|
|
} else if (req.uri.path == '/watch-together-0.2.0-linux-x64.tar.gz') {
|
|
tarHits++;
|
|
req.response.contentLength = tarball.length;
|
|
req.response.add(tarball);
|
|
} else {
|
|
req.response.statusCode = 404;
|
|
}
|
|
await req.response.close();
|
|
});
|
|
}
|
|
|
|
Future<void> stop() => http.close(force: true);
|
|
}
|
|
|
|
// The updater itself is tested in packages/core/test/updater_test.dart
|
|
// (fixtures above mirror it); this covers the app's UpdateController.
|
|
void main() {
|
|
group('in-app controller', () {
|
|
late Directory work;
|
|
late _Server server;
|
|
|
|
setUp(() async {
|
|
work = await _work();
|
|
server = _Server(await _releaseTarball(work));
|
|
await server.start();
|
|
});
|
|
|
|
tearDown(() async {
|
|
await server.stop();
|
|
if (await work.exists()) await work.delete(recursive: true);
|
|
});
|
|
|
|
test('check, download with progress, apply, restart same binary',
|
|
() async {
|
|
final root = await _fakeRoot(work);
|
|
final restarts = <(String, List<String>)>[];
|
|
launchArgs = const ['--some-flag'];
|
|
final c = UpdateController(
|
|
currentVersion: '0.1.0',
|
|
manifestUrl: server.manifestUrl,
|
|
executablePath: '${root.path}/watch_together',
|
|
restart: (exe, args) async => restarts.add((exe, args)));
|
|
await c.checkOnce();
|
|
expect(c.status.value.phase, UpdatePhase.available);
|
|
expect(c.status.value.canInstall, isTrue);
|
|
expect(c.status.value.manifest!.isRequired, isFalse);
|
|
final progress = <double>[];
|
|
c.status.addListener(() {
|
|
final p = c.status.value.progress;
|
|
if (p != null) progress.add(p);
|
|
});
|
|
await c.install();
|
|
expect(progress.last, 1.0);
|
|
expect(c.status.value.phase, UpdatePhase.restarting);
|
|
expect(await File('${root.path}/watch_together').readAsString(),
|
|
'binary-v2');
|
|
expect(_isExec('${root.path}/watch_together'), isTrue);
|
|
expect(restarts.single.$1, '${root.path}/watch_together');
|
|
expect(restarts.single.$2, ['--some-flag']);
|
|
}, skip: _posixOnly);
|
|
|
|
test('newer protocol is a required update, cannot be dismissed',
|
|
() async {
|
|
server.manifestExtra = {'protocol': RelayConstants.protoVersion + 1};
|
|
final c = UpdateController(
|
|
currentVersion: '0.1.0',
|
|
manifestUrl: server.manifestUrl,
|
|
executablePath: '${work.path}/stray-binary',
|
|
restart: (_, _) async {});
|
|
await c.checkOnce();
|
|
expect(c.status.value.manifest!.isRequired, isTrue);
|
|
expect(c.status.value.canInstall, isFalse);
|
|
c.dismiss();
|
|
expect(c.status.value.phase, UpdatePhase.available);
|
|
});
|
|
|
|
test('failed check stays quiet; dev builds skip the check', () async {
|
|
server.brokenManifest = true;
|
|
final c = UpdateController(
|
|
currentVersion: '0.1.0',
|
|
manifestUrl: server.manifestUrl,
|
|
executablePath: '${work.path}/stray-binary',
|
|
restart: (_, _) async {});
|
|
await c.checkOnce();
|
|
expect(c.status.value.phase, UpdatePhase.idle);
|
|
server.brokenManifest = false;
|
|
final dev = UpdateController(
|
|
currentVersion: '0.0.0-dev',
|
|
manifestUrl: server.manifestUrl,
|
|
executablePath: '${work.path}/stray-binary',
|
|
restart: (_, _) async {});
|
|
await dev.checkOnce();
|
|
expect(dev.status.value.phase, UpdatePhase.idle);
|
|
});
|
|
|
|
test('failed install shows error, install untouched, retry works',
|
|
() async {
|
|
server.manifestExtra = {'sha256': '0' * 64};
|
|
final root = await _fakeRoot(work);
|
|
var restarted = false;
|
|
final c = UpdateController(
|
|
currentVersion: '0.1.0',
|
|
manifestUrl: server.manifestUrl,
|
|
executablePath: '${root.path}/watch_together',
|
|
restart: (_, _) async => restarted = true);
|
|
await c.checkOnce();
|
|
await c.install();
|
|
expect(c.status.value.phase, UpdatePhase.failed);
|
|
expect(c.status.value.error, contains('sha256'));
|
|
expect(restarted, isFalse);
|
|
expect(await File('${root.path}/watch_together').readAsString(),
|
|
'binary-v1');
|
|
}, skip: _posixOnly);
|
|
});
|
|
}
|