Files
TPSchedule/mobile/test/services/calendar_service_test.dart

146 lines
4.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:hepan_everyday/services/android_calendar_service.dart';
import 'package:hepan_everyday/services/calendar_service.dart';
import 'package:hepan_everyday/domain/schedule_engine.dart';
import '../domain/schedule_engine_test.dart' show course;
class CalendarProvider extends CalendarGateway {
bool access = true, failAfterFirst = false;
final records = <String, CalendarRecord>{};
final drafts = <String, CalendarEventDraft>{};
int inserts = 0;
@override
Future<bool> requestAccess() async => access;
@override
Future<List<DeviceCalendar>> calendars() async => [
const DeviceCalendar('local', '专用测试日历'),
const DeviceCalendar('other', '另一测试日历'),
];
@override
Future<List<CalendarRecord>> ownedEvents(String calendarId) async {
if (!access) throw StateError('permission denied');
return records.values.where((r) => r.calendarId == calendarId).toList();
}
@override
Future<String> save(CalendarEventDraft event, String? eventId) async {
if (failAfterFirst && inserts >= 1) {
throw StateError('provider unavailable');
}
final id = eventId ?? '${++inserts}';
records[id] = CalendarRecord(
id: id,
key: event.key,
calendarId: event.calendarId,
owned: true,
);
drafts[id] = event;
return id;
}
@override
Future<void> delete(String eventId, String expectedKey) async {
final r = records[eventId];
if (r?.owned == true && r?.key == expectedKey) {
records.remove(eventId);
drafts.remove(eventId);
}
}
}
void main() {
setUpAll(tzdata.initializeTimeZones);
final term = Semester(startDate: DateTime(2026, 9, 9), totalWeeks: 5);
late CalendarProvider provider;
late AndroidCalendarService service;
setUp(() {
provider = CalendarProvider();
service = AndroidCalendarService(provider);
});
test(
'repeat import creates one event with UTC instants and school metadata',
() async {
final events = ScheduleEngine.occurrences([course(last: 2)], term);
final ids = await service.sync(events, 'local', {});
expect(ids, hasLength(1));
await service.sync(events, 'local', ids);
expect(provider.records, hasLength(1));
expect(provider.inserts, 1);
final event = provider.drafts.values.single;
expect(
event.startMs,
DateTime.utc(2026, 9, 9, 0, 30).millisecondsSinceEpoch,
);
expect(
event.endMs,
DateTime.utc(2026, 9, 9, 2, 5).millisecondsSinceEpoch,
);
expect(event.description, contains('示例教师'));
},
);
test(
'lost mapping after an interrupted sync recovers via owned event identity',
() async {
final events = ScheduleEngine.occurrences([
course(id: 'a'),
course(id: 'b'),
], term);
provider.failAfterFirst = true;
await expectLater(service.sync(events, 'local', {}), throwsStateError);
expect(provider.records, hasLength(1));
provider.failAfterFirst = false;
await service.sync(events, 'local', {});
expect(provider.records, hasLength(2));
expect(provider.inserts, 2);
},
);
test(
'manual deletion is repaired and unrelated events are preserved',
() async {
final events = ScheduleEngine.occurrences([course()], term);
final ids = await service.sync(events, 'local', {});
expect(ids, hasLength(1));
provider.records.clear();
provider.records['private'] = const CalendarRecord(
id: 'private',
key: 'private',
calendarId: 'local',
owned: false,
);
final updated = await service.sync(events, 'local', ids);
expect(updated, hasLength(1));
expect(provider.records, hasLength(2));
await service.deleteOwned(updated);
expect(provider.records.keys, ['private']);
},
);
test(
'changed ownership marker cannot be deleted using an old stored mapping',
() async {
provider.records['17'] = const CalendarRecord(
id: '17',
key: 'math/2026-09-09',
calendarId: 'local',
owned: false,
);
await service.deleteOwned({'math/2026-09-09': '17'});
expect(provider.records.keys, ['17']);
},
);
test('replacing timetable removes stale owned events within selected calendar only', () async {
provider.records['other'] = const CalendarRecord(
id: 'other',
key: 'old',
calendarId: 'different',
owned: true,
);
final events = ScheduleEngine.occurrences([course()], term);
final ids = await service.sync(events, 'local', {});
expect(ids, hasLength(1));
await service.sync([], 'local', ids);
expect(provider.records.keys, ['other']);
});
}