added guardian login with views for their assigned childs

This commit is contained in:
2026-09-20 11:11:58 +02:00
parent e4e2b1a4fb
commit 67c935c05b
117 changed files with 4784 additions and 1514 deletions
@@ -0,0 +1,107 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/auth_link/device_binding.dart';
import 'package:marianum_mobile/auth_link/guardian_login_link.dart';
import 'package:marianum_mobile/auth_link/pending_guardian_request.dart';
void main() {
final live = Uri.parse('https://connect.marianum-fulda.de');
group('GuardianLoginLink.parse', () {
test('accepts a link of the active server', () {
final link = GuardianLoginLink.parse(
Uri.parse(
'https://connect.marianum-fulda.de/app/guardian-login?rid=r1&lt=t1',
),
apiBase: live,
);
expect(link?.requestId, 'r1');
expect(link?.linkToken, 't1');
});
test('rejects links of another server', () {
expect(
GuardianLoginLink.parse(
Uri.parse(
'https://connect-beta.marianum-fulda.de/app/guardian-login?rid=r&lt=t',
),
apiBase: live,
),
isNull,
);
});
test('respects a path prefix of a custom server', () {
final custom = Uri.parse('https://dev.example.org/connect/');
expect(
GuardianLoginLink.parse(
Uri.parse(
'https://dev.example.org/connect/app/guardian-login?rid=r&lt=t',
),
apiBase: custom,
),
isNotNull,
);
});
test('rejects other paths, plain http and missing parameters', () {
for (final raw in [
'https://connect.marianum-fulda.de/app/other?rid=r&lt=t',
'http://connect.marianum-fulda.de/app/guardian-login?rid=r&lt=t',
'https://connect.marianum-fulda.de/app/guardian-login?rid=r',
'https://connect.marianum-fulda.de/app/guardian-login?lt=t',
]) {
expect(
GuardianLoginLink.parse(Uri.parse(raw), apiBase: live),
isNull,
reason: raw,
);
}
});
});
group('DeviceBinding', () {
test('challenge is the base64url SHA-256 of the secret, unpadded', () {
// RFC 7636 appendix B test vector.
expect(
DeviceBinding.challengeFor(
'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk',
),
'E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM',
);
});
test('secrets are random and URL-safe', () {
final a = DeviceBinding.generateSecret();
final b = DeviceBinding.generateSecret();
expect(a, isNot(b));
expect(a, matches(RegExp(r'^[A-Za-z0-9_-]{43}$')));
});
});
group('PendingGuardianRequest', () {
final request = PendingGuardianRequest(
requestId: 'r1',
email: 'e@x.de',
deviceSecret: 's',
expiresAt: DateTime.utc(2026, 9, 19, 12, 15),
resendAvailableAt: DateTime.utc(2026, 9, 19, 12, 1),
);
test('round-trips through JSON', () {
final copy = PendingGuardianRequest.fromJson(request.toJson())!;
expect(copy.requestId, 'r1');
expect(copy.email, 'e@x.de');
expect(copy.expiresAt, request.expiresAt);
expect(copy.codeLength, 6);
});
test('corrupt JSON reads as absent', () {
expect(PendingGuardianRequest.fromJson({'requestId': 1}), isNull);
});
test('expires at expiresAt', () {
expect(request.isExpired(DateTime.utc(2026, 9, 19, 12, 14)), isFalse);
expect(request.isExpired(DateTime.utc(2026, 9, 19, 12, 15)), isTrue);
});
});
}