feat: persist timetable and validate confirmed imports

This commit is contained in:
Codex
2026-10-12 23:18:51 +08:00
parent ae8e347e49
commit 808af007a2
13 changed files with 852 additions and 0 deletions
@@ -0,0 +1,55 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/data/json_schedule_import.dart';
import 'package:hepan_everyday/domain/course.dart';
Map<String, dynamic> row({String id = 'a', int day = 1}) => {
'id': id,
'name': '矩阵论',
'teacher': '老师',
'room': '教室',
'weekday': day,
'startPeriod': 1,
'endPeriod': 2,
'weeks': [1, 3, 5],
};
String input(List<dynamic> courses, {int version = 1, bool demo = false}) =>
jsonEncode({'schemaVersion': version, 'isDemo': demo, 'courses': courses});
void main() {
test('file import normalizes weeks and cannot claim school provenance', () {
final parsed = JsonScheduleImport.parse(input([row()]));
expect(parsed.courses, hasLength(1));
expect(parsed.courses.single.weeks, {1, 3, 5});
expect(parsed.source, CourseSource.file);
});
test('one invalid course rejects the whole draft', () {
expect(
() => JsonScheduleImport.parse(input([row(), row(id: 'b', day: 8)])),
throwsFormatException,
);
});
test('duplicate identifiers and unsupported schema are rejected', () {
expect(
() => JsonScheduleImport.parse(input([row(), row()])),
throwsFormatException,
);
expect(
() => JsonScheduleImport.parse(input([], version: 2)),
throwsFormatException,
);
});
test('demo import is explicitly separated from real records', () {
expect(
JsonScheduleImport.parse(input([row()], demo: true)).source,
CourseSource.demo,
);
});
test('valid but oversized JSON is rejected before accepting a course', () {
final large = row()..['name'] = List.filled(2100000, 'x').join();
expect(
() => JsonScheduleImport.parse(input([large])),
throwsFormatException,
);
});
}
+57
View File
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/data/file_snapshot_store.dart';
import 'package:hepan_everyday/domain/app_snapshot.dart';
import 'package:hepan_everyday/domain/app_settings.dart';
import '../domain/schedule_engine_test.dart' show course;
void main() {
late Directory directory;
setUp(() async {
directory = await Directory.systemTemp.createTemp('hepan-test-');
});
tearDown(() async {
await directory.delete(recursive: true);
});
test(
'reopening restores courses, settings and calendar identifiers',
() async {
final original = AppSnapshot(
settings: const AppSettings(reminderMinutes: 30),
courses: [course()],
calendarEventIds: {'math/2026-09-09': '17'},
);
await FileSnapshotStore(directory).save(original);
final loaded = await FileSnapshotStore(directory).load();
expect(loaded, isNotNull);
expect(loaded!.courses.single.name, '矩阵论');
expect(loaded.settings.reminderMinutes, 30);
expect(loaded.calendarEventIds, {'math/2026-09-09': '17'});
},
);
test(
'damaged snapshot reports failure instead of returning empty courses',
() async {
await File('${directory.path}/hepan_snapshot.json')
.writeAsString('damaged');
await expectLater(
FileSnapshotStore(directory).load(),
throwsFormatException,
);
},
);
test('valid previous snapshot is available for explicit recovery', () async {
final store = FileSnapshotStore(directory);
await store.save(
AppSnapshot(settings: const AppSettings(reminderMinutes: 15)),
);
await store.save(
AppSnapshot(settings: const AppSettings(reminderMinutes: 30)),
);
await File('${directory.path}/hepan_snapshot.json')
.writeAsString('damaged');
expect((await store.restoreBackup()).settings.reminderMinutes, 15);
});
}