212 lines
8.0 KiB
Dart
212 lines
8.0 KiB
Dart
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<int> 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(
|
|
'''<header>ACCOUNT_SECRET</header>${fixture()}
|
|
<input name="ticket" value="AUTH_SECRET"><script>const cookie="AUTH_SECRET";</script>''',
|
|
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<UnsupportedSchoolLayout>()),
|
|
);
|
|
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('<input type="password">${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(
|
|
'<select id="drpxq">',
|
|
'<select id="drpxq" onchange="AUTH_SECRET()">',
|
|
)
|
|
.replaceFirst(
|
|
'示例教师甲',
|
|
'<a href="https://evil.test/?ticket=AUTH_SECRET" onclick="AUTH_SECRET()">示例教师甲</a><input name="cookie" value="AUTH_SECRET">',
|
|
),
|
|
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<input name="password" value="SECRET"><input name="params" value="ticket=SECRET"><a href="https://evil.test/?ticket=SECRET" onclick="steal()">链接</a><script>var token="SECRET";</script>''',
|
|
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('<table id="manualArrangeCourseTable"></table>', uri);
|
|
expect(store.export(), isNotNull);
|
|
store.clear();
|
|
expect(store.export(), isNull);
|
|
store.remember(
|
|
'<table>secret</table>',
|
|
Uri.parse('https://idas.uestc.edu.cn/authserver/login'),
|
|
);
|
|
expect(store.export(), isNull);
|
|
store.remember(
|
|
'<input type="password"><table id="manualArrangeCourseTable"></table>',
|
|
uri,
|
|
);
|
|
expect(store.export(), isNull);
|
|
});
|
|
}
|