fix: follow undergraduate login links and capture live history URLs

This commit is contained in:
Codex
2026-10-15 17:17:54 +08:00
parent 79d268fd5e
commit defe7fd50e
14 changed files with 213 additions and 32 deletions
@@ -6,6 +6,51 @@ import 'package:hepan_everyday/features/school/undergraduate/page_probe.dart';
void main() {
final uri = Uri.parse('https://eams.uestc.edu.cn/eams/');
test(
'legacy HTTP continuation upgrade is limited to the exact school system',
() {
expect(
UndergraduateNavigationPolicy.secureIdentityRedirect(
Uri.parse(
'http://eams.uestc.edu.cn/eams/home.action?login=1&next=%2Feams%2F#continue',
),
).toString(),
'https://eams.uestc.edu.cn/eams/home.action?login=1&next=%2Feams%2F#continue',
);
for (final address in [
'http://eams.uestc.edu.cn.attacker.test/eams/home.action',
'http://user@eams.uestc.edu.cn/eams/home.action',
'http://eams.uestc.edu.cn:8080/eams/home.action',
'http://eams.uestc.edu.cn/other/action',
]) {
expect(
UndergraduateNavigationPolicy.secureIdentityRedirect(
Uri.parse(address),
),
isNull,
);
}
},
);
test(
'decoder still requires captured URL to match the live document URL',
() {
expect(
() => UndergraduatePageEvidence.decode({
'topUrl': 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
'authenticated': true,
'pages': [
{
'kind': 'timetable',
'url': 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
'html': 'table',
},
],
}, uri),
throwsFormatException,
);
},
);
test(
'navigation and capture distinguish official data from authentication',
() {
@@ -31,6 +31,59 @@ void main() {
session = UndergraduateSession();
});
tearDown(() => session.dispose());
test('live URL cannot cross origin or change again during capture', () async {
await session.openTimetable();
final content = File('test/fixtures/school/undergraduate-timetable.html')
.readAsStringSync();
platform.web.javascript = (_) async => underEvidence(
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
html: content,
);
for (final address in [
'https://other.test/eams/courseTableForStd.action',
'https://idas.uestc.edu.cn/authserver/login',
'http://eams.uestc.edu.cn/eams/courseTableForStd.action',
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=503',
]) {
platform.web.documentUrl = address;
await session.readCurrentTimetable();
expect(session.pendingDraft, isNull);
expect(session.message, contains('未完成'));
}
});
test('visible timetable imports after same-document history changes native URL does not follow', () async {
await session.openTimetable();
platform.web.documentUrl = 'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523';
final content = File('test/fixtures/school/undergraduate-timetable.html')
.readAsStringSync();
platform.web.javascript = (_) async => underEvidence(
'https://eams.uestc.edu.cn/eams/courseTableForStd.action?semester.id=523',
html: content,
);
platform.delegate.finished(platform.web.url!);
await flushSchoolEvents();
expect(session.statusLabel, '已连接');
await session.readCurrentTimetable();
expect(session.pendingDraft!.courses, hasLength(18));
});
test(
'duplicate login continuation upgrades exact school HTTP to HTTPS',
() async {
await session.openTimetable();
final decision = await platform.delegate.navigation(
NavigationRequest(
url: 'http://eams.uestc.edu.cn/eams/home.action?login=1',
isMainFrame: true,
),
);
expect(decision, NavigationDecision.prevent);
expect(
platform.web.loads.last,
'https://eams.uestc.edu.cn/eams/home.action?login=1',
);
expect(session.message, isNot(contains('未完成')));
},
);
test(
'page-finished during an old probe is retried on the redirected portal',
() async {
+4 -1
View File
@@ -44,6 +44,7 @@ class FakeSchoolWeb extends PlatformWebViewController {
FakeSchoolWeb(super.params) : super.implementation();
final loads = <String>[];
String? url;
String? documentUrl;
Future<Object> Function(String)? javascript;
Future<void> Function(Uri)? request;
@override
@@ -63,7 +64,9 @@ class FakeSchoolWeb extends PlatformWebViewController {
Future<String?> currentUrl() async => url;
@override
Future<Object> runJavaScriptReturningResult(String script) =>
javascript?.call(script) ?? Future.value(false);
script == 'location.href'
? Future.value(documentUrl ?? url ?? 'about:blank')
: javascript?.call(script) ?? Future.value(false);
@override
Future<void> clearCache() async {}
@override