import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:hepan_everyday/services/school_page_diagnostic.dart'; import 'package:hepan_everyday/features/school/undergraduate/timetable_adapter.dart'; import 'package:hepan_everyday/features/school/school_page_adapter.dart'; import 'package:hepan_everyday/features/school/school_exam_adapter.dart'; import '../school/recognized_timetable_test.dart' show fixture; import '../school/exam_adapter_test.dart' show examHtml; void exportContractFixture(String name, List bytes) { final directory = Platform.environment['TPSCHEDULE_DIAGNOSTIC_EXPORT_DIR']; if (directory != null) File('$directory/$name').writeAsBytesSync(bytes); } void main() { test( 'graduate timetable preserves term, periods and course parser evidence', () { final store = SchoolPageDiagnostic(); addTearDown(store.clear); final uri = Uri.parse( 'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73', ); store.remember( '''
ACCOUNT_SECRET
${fixture()} ''', uri, ); expect(store.export(), isNotNull); final output = utf8.decode(store.export()!); exportContractFixture('client-graduate-timetable.html', store.export()!); final draft = SchoolPageAdapter().extract(output, uri); expect(draft.schoolTerm!.id, 73); expect(draft.courses, hasLength(4)); expect( draft.courses .singleWhere((c) => c.weekday == 2 && c.startPeriod == 3) .teacher, '示例教师乙、示例教师丙', ); expect(output, isNot(contains('SECRET'))); expect(output, isNot(contains('name="ticket"'))); }, ); test('graduate exams preserve pending arrangements and selected term', () { final store = SchoolPageDiagnostic(); addTearDown(store.clear); store.remember( examHtml(room: '示例考场'), Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/kskcxx/index?xq=73'), ); expect(store.export(), isNotNull); exportContractFixture('client-graduate-exams.html', store.export()!); final exam = SchoolExamAdapter.extract(utf8.decode(store.export()!)); expect(exam.termId, '73'); expect(exam.records.single.room, '示例考场'); expect(exam.records.single.time, isEmpty); }); test( 'graduate HTTP frame and malformed parser fields remain diagnosable', () { final store = SchoolPageDiagnostic(); addTearDown(store.clear); final uri = Uri.parse('http://yjsjy.uestc.edu.cn/pyxx/home/index'); final malformed = fixture().replaceFirst('1-3周', '未知周'); store.remember(malformed, uri); expect(store.export(), isNotNull); final output = utf8.decode(store.export()!); expect(output, contains('未知周')); expect( () => SchoolPageAdapter().extract( output, Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'), ), throwsA(isA()), ); for (final address in [ 'https://yjsjy.uestc.edu.cn.evil.test/pyxx/home/index', 'https://yjsjy.uestc.edu.cn:444/pyxx/home/index', 'http://yjsjy.uestc.edu.cn:8080/pyxx/home/index', 'https://user@yjsjy.uestc.edu.cn/pyxx/home/index', 'https://yjsjy.uestc.edu.cn/other/index', ]) { store.remember(fixture(), Uri.parse(address)); expect(store.export(), isNull, reason: address); } store.remember('${fixture()}', uri); expect(store.export(), isNull); }, ); test('graduate export removes events, URLs and account controls and expires at ten minutes', () { var now = DateTime.utc(2026, 10, 24); final store = SchoolPageDiagnostic(clock: () => now); addTearDown(store.clear); store.remember( fixture() .replaceFirst( '', ) .replaceFirst( '示例教师甲', '示例教师甲', ), Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'), ); expect(store.export(), isNotNull); final output = utf8.decode(store.export()!); expect(output, isNot(contains('AUTH_SECRET'))); expect(output, isNot(contains('evil.test'))); expect(output, isNot(contains('onchange'))); expect(output, contains('示例教师甲')); now = now.add(const Duration(minutes: 10)); expect(store.export(), isNull); }); final uri = Uri.parse( 'https://eams.uestc.edu.cn/eams/courseTableForStd.action', ); test( 'leading comments do not remove course declarations from the evidence', () { final store = SchoolPageDiagnostic(); addTearDown(store.clear); final source = File('test/fixtures/school/undergraduate-timetable.html') .readAsStringSync() .replaceFirst( 'var unitCount = 12;', '// units; keep this declaration\nvar unitCount = 12;', ) .replaceAll( 'activity = new TaskActivity', '/* course; declaration */activity = new TaskActivity', ); expect( UndergraduateTimetableAdapter.extract(source, uri).courses, hasLength(18), ); store.remember(source, uri); expect( UndergraduateTimetableAdapter.extract( utf8.decode(store.export()!), uri, ).courses, hasLength(18), ); }, ); test('malformed activity and index statements remain malformed in the exported evidence', () { final store = SchoolPageDiagnostic(); addTearDown(store.clear); final fixture = File('test/fixtures/school/undergraduate-timetable.html') .readAsStringSync(); for (final bad in [ fixture.replaceFirst('"教师1"', 'null'), fixture.replaceFirst('2*unitCount+0', 'unknown()'), ]) { expect( () => UndergraduateTimetableAdapter.extract(bad, uri), throwsFormatException, ); store.remember(bad, uri); final output = utf8.decode(store.export()!); expect( () => UndergraduateTimetableAdapter.extract(output, uri), throwsFormatException, ); expect(output, contains(bad.contains('null') ? 'null' : 'unknown()')); } }); test('diagnostic snapshot preserves parser evidence but removes login material and active HTML', () { var now = DateTime.utc(2026, 10, 16, 5); final store = SchoolPageDiagnostic(clock: () => now); final fixture = File('test/fixtures/school/undergraduate-timetable.html') .readAsStringSync(); store.remember( '''$fixture链接''', uri, ); final output = utf8.decode(store.export()!); expect(output, isNot(contains('SECRET'))); expect(output, isNot(contains('onclick'))); expect(output, isNot(contains('evil.test'))); expect(output, contains('Content-Security-Policy')); expect( UndergraduateTimetableAdapter.extract(output, uri).courses, hasLength(18), ); now = now.add(const Duration(minutes: 11)); expect(store.export(), isNull); }); test('only trusted school evidence can be kept and logout clears it', () { final store = SchoolPageDiagnostic(); store.remember('
', uri); expect(store.export(), isNotNull); store.clear(); expect(store.export(), isNull); store.remember( 'secret
', Uri.parse('https://idas.uestc.edu.cn/authserver/login'), ); expect(store.export(), isNull); store.remember( '
', uri, ); expect(store.export(), isNull); }); }