What every Halkyon app's shell needs, taken from Halkyon Learn's and set on halkyon_base's client: the instances a device keeps an account on, the first start's tiles and where an instance keeps people's data, the account switcher; signing in anonymously, with GitHub or an email code, switching person and the start screen; the gates that build an app's dependencies per instance and per person; and the sync service with its status, Settings card and banner. Texts of its own in English and German take the app's name; what only the app can say, it passes in. Fakes for apps' tests are in testing.dart. An instance's basics now name the app it serves (`app`, set by the host's configuration), and an app accepts only an instance of its own. SerialQueue and CoalescingRunner move into halkyon_core. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
120 lines
3.1 KiB
Dart
120 lines
3.1 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
import 'package:serverpod_offline_sync_client/offline_sync.dart';
|
|
|
|
void main() {
|
|
group('AppCompatibility', () {
|
|
late int asked;
|
|
late Object? answer;
|
|
|
|
AppCompatibility compatibility(int? appBuild) => AppCompatibility(
|
|
appBuild: appBuild,
|
|
minimumAppBuild: () {
|
|
asked++;
|
|
return switch (answer) {
|
|
final Exception error => Future.error(error),
|
|
_ => Future.value(answer as int?),
|
|
};
|
|
},
|
|
);
|
|
|
|
setUp(() {
|
|
asked = 0;
|
|
answer = null;
|
|
});
|
|
|
|
test('lets any build sync when the server names no minimum', () async {
|
|
await compatibility(1).check();
|
|
});
|
|
|
|
test('lets a build at or above the minimum sync', () async {
|
|
answer = 3;
|
|
await compatibility(3).check();
|
|
await compatibility(4).check();
|
|
});
|
|
|
|
test('refuses a build below the minimum', () async {
|
|
answer = 3;
|
|
await expectLater(
|
|
compatibility(2).check(),
|
|
throwsA(isA<AppServerMismatchException>()),
|
|
);
|
|
});
|
|
|
|
test('asks the server once per launch', () async {
|
|
answer = 1;
|
|
final check = compatibility(1);
|
|
await check.check();
|
|
await check.check();
|
|
expect(asked, 1);
|
|
});
|
|
|
|
test('asks again after the question failed', () async {
|
|
answer = Exception('No connection');
|
|
final check = compatibility(1);
|
|
await expectLater(check.check(), throwsException);
|
|
|
|
answer = 1;
|
|
await check.check();
|
|
expect(asked, 2);
|
|
});
|
|
|
|
test('never refuses a build without a number, and never asks', () async {
|
|
answer = 99;
|
|
await compatibility(null).check();
|
|
expect(asked, 0);
|
|
});
|
|
});
|
|
|
|
group('compatibleSync', () {
|
|
test('checks before syncing', () async {
|
|
var synced = false;
|
|
final sync = compatibleSync(
|
|
({required onMerge}) {
|
|
synced = true;
|
|
return Future.value();
|
|
},
|
|
AppCompatibility(appBuild: 1, minimumAppBuild: () => Future.value(2)),
|
|
);
|
|
|
|
await expectLater(
|
|
sync(onMerge: () {}),
|
|
throwsA(isA<AppServerMismatchException>()),
|
|
);
|
|
expect(synced, isFalse);
|
|
});
|
|
|
|
test("tells when the synced tables don't match the server's", () async {
|
|
final sync = compatibleSync(
|
|
({required onMerge}) => Future.error(
|
|
const OfflineSyncTablesHashMismatchException(
|
|
received: 'server',
|
|
expected: 'app',
|
|
),
|
|
),
|
|
AppCompatibility(
|
|
appBuild: 1,
|
|
minimumAppBuild: Future<int?>.value,
|
|
),
|
|
);
|
|
|
|
await expectLater(
|
|
sync(onMerge: () {}),
|
|
throwsA(isA<AppServerMismatchException>()),
|
|
);
|
|
});
|
|
|
|
test('passes other failures on as they are', () async {
|
|
final failure = Exception('No connection');
|
|
final sync = compatibleSync(
|
|
({required onMerge}) => Future.error(failure),
|
|
AppCompatibility(
|
|
appBuild: 1,
|
|
minimumAppBuild: Future<int?>.value,
|
|
),
|
|
);
|
|
|
|
await expectLater(sync(onMerge: () {}), throwsA(same(failure)));
|
|
});
|
|
});
|
|
}
|