added support for 2fa login with browser flow

This commit is contained in:
2026-08-10 20:00:38 +02:00
parent 889d8f67c5
commit ccb22a497d
13 changed files with 805 additions and 35 deletions
@@ -0,0 +1,108 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:marianum_mobile/api/marianumcloud/login_flow/login_flow_api.dart';
void main() {
group('LoginFlowInit.fromJson', () {
test('parses a complete init response', () {
final init = LoginFlowInit.fromJson({
'poll': {
'token': 'abc123',
'endpoint': 'https://cloud.example.org/login/v2/poll',
},
'login': 'https://cloud.example.org/login/v2/flow/xyz',
});
expect(init.loginUrl, 'https://cloud.example.org/login/v2/flow/xyz');
expect(init.pollToken, 'abc123');
expect(init.pollEndpoint, 'https://cloud.example.org/login/v2/poll');
});
test('throws on missing login url', () {
expect(
() => LoginFlowInit.fromJson({
'poll': {'token': 't', 'endpoint': 'e'},
}),
throwsFormatException,
);
});
test('throws on missing poll token or endpoint', () {
expect(
() => LoginFlowInit.fromJson({
'poll': {'endpoint': 'e'},
'login': 'l',
}),
throwsFormatException,
);
expect(
() => LoginFlowInit.fromJson({
'poll': {'token': 't'},
'login': 'l',
}),
throwsFormatException,
);
expect(
() => LoginFlowInit.fromJson({'login': 'l'}),
throwsFormatException,
);
});
});
group('LoginFlowCredentials.fromJson', () {
test('parses a complete poll response', () {
final credentials = LoginFlowCredentials.fromJson({
'server': 'https://cloud.example.org',
'loginName': 'jdoe',
'appPassword': 'secret-app-password',
});
expect(credentials.server, 'https://cloud.example.org');
expect(credentials.loginName, 'jdoe');
expect(credentials.appPassword, 'secret-app-password');
});
test('tolerates a missing server field', () {
final credentials = LoginFlowCredentials.fromJson({
'loginName': 'jdoe',
'appPassword': 'secret',
});
expect(credentials.server, '');
});
test('throws on missing loginName or appPassword', () {
expect(
() => LoginFlowCredentials.fromJson({'appPassword': 'secret'}),
throwsFormatException,
);
expect(
() => LoginFlowCredentials.fromJson({'loginName': 'jdoe'}),
throwsFormatException,
);
expect(
() => LoginFlowCredentials.fromJson({
'loginName': 'jdoe',
'appPassword': '',
}),
throwsFormatException,
);
});
});
group('LoginFlowApi.loginNameMatches', () {
test('matches case-insensitively and ignores surrounding whitespace', () {
expect(
LoginFlowApi.loginNameMatches(expected: 'jdoe', actual: 'JDoe'),
isTrue,
);
expect(
LoginFlowApi.loginNameMatches(expected: 'jdoe', actual: ' jdoe '),
isTrue,
);
});
test('rejects a different account', () {
expect(
LoginFlowApi.loginNameMatches(expected: 'jdoe', actual: 'other'),
isFalse,
);
});
});
}
@@ -53,6 +53,45 @@ void main() {
});
});
group('registrationTypesFor', () {
test('password accounts maintain both registrations', () {
expect(
PushRegistration.registrationTypesFor(
usesLoginFlow: false,
hasTalkAppPassword: false,
),
PushRegistrationType.values,
);
expect(
PushRegistration.registrationTypesFor(
usesLoginFlow: false,
hasTalkAppPassword: true,
),
PushRegistrationType.values,
);
});
test('flow account without talk app password is talk-only', () {
expect(
PushRegistration.registrationTypesFor(
usesLoginFlow: true,
hasTalkAppPassword: false,
),
[PushRegistrationType.talk],
);
});
test('flow account with second (talk) app password maintains both', () {
expect(
PushRegistration.registrationTypesFor(
usesLoginFlow: true,
hasTalkAppPassword: true,
),
PushRegistrationType.values,
);
});
});
group('pushTokenVariant', () {
test('general uses the raw token, talk appends the suffix', () {
expect(pushTokenVariant('tok', PushRegistrationType.general), 'tok');