269 lines
8.6 KiB
Dart
269 lines
8.6 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:please_pay_me/core/config/app_config.dart';
|
|
import 'package:please_pay_me/core/storage/session_storage.dart';
|
|
import 'package:please_pay_me/features/auth/login_screen.dart';
|
|
import 'package:please_pay_me/features/auth/session_controller.dart';
|
|
import 'package:please_pay_me/features/auth/telegram_login.dart';
|
|
import 'package:please_pay_me/features/auth/yandex_login.dart';
|
|
import 'package:please_pay_me/theme/theme.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
import 'support/test_setup.dart';
|
|
|
|
void main() {
|
|
setUpAll(setUpTestEnvironment);
|
|
|
|
group('resolveApiBaseUrl', () {
|
|
test('an explicitly configured API address wins', () {
|
|
expect(
|
|
resolveApiBaseUrl(
|
|
cabinetUrl: 'https://cabinet.example.com',
|
|
configuredApiBaseUrl: 'https://api.example.com/',
|
|
),
|
|
'https://api.example.com',
|
|
);
|
|
});
|
|
|
|
test('falls back to the cabinet origin, dropping the path', () {
|
|
expect(
|
|
resolveApiBaseUrl(cabinetUrl: 'https://ppm.example.com/cabinet/budgets'),
|
|
'https://ppm.example.com',
|
|
);
|
|
});
|
|
|
|
test('keeps a non-default port', () {
|
|
expect(
|
|
resolveApiBaseUrl(cabinetUrl: 'http://192.168.0.10:8080/'),
|
|
'http://192.168.0.10:8080',
|
|
);
|
|
});
|
|
|
|
test('assumes https when the scheme is omitted', () {
|
|
expect(resolveApiBaseUrl(cabinetUrl: 'ppm.example.com/'), 'https://ppm.example.com');
|
|
});
|
|
});
|
|
|
|
group('resolveCabinetLoginUri', () {
|
|
test('opens /login on the cabinet origin', () {
|
|
expect(
|
|
resolveCabinetLoginUri('https://ppm.example.com'),
|
|
Uri.parse('https://ppm.example.com/login'),
|
|
);
|
|
});
|
|
|
|
test('keeps an explicit path', () {
|
|
expect(
|
|
resolveCabinetLoginUri('https://ppm.example.com/cabinet'),
|
|
Uri.parse('https://ppm.example.com/cabinet'),
|
|
);
|
|
});
|
|
});
|
|
|
|
group('navigation helpers', () {
|
|
test('detects Telegram deep links', () {
|
|
expect(isExternalAuthScheme(Uri.parse('tg://resolve?domain=bot')), isTrue);
|
|
expect(isExternalAuthScheme(Uri.parse('https://oauth.telegram.org/auth')), isFalse);
|
|
});
|
|
|
|
test('detects Telegram OAuth hosts', () {
|
|
expect(isTelegramOAuthHost('oauth.telegram.org'), isTrue);
|
|
expect(isTelegramOAuthHost('ppm.example.com'), isFalse);
|
|
});
|
|
});
|
|
|
|
group('telegram auth bridge', () {
|
|
test('probe script reads the key the web cabinet writes', () {
|
|
// Must stay in sync with SESSION_KEY in web/src/api.ts.
|
|
expect(telegramTokenProbeJs, contains("getItem('ppm_session_jwt')"));
|
|
expect(telegramTokenProbeJs, contains('$telegramAuthChannel.postMessage'));
|
|
});
|
|
|
|
test('parses the posted payload', () {
|
|
expect(parseTelegramAuthMessage(jsonEncode({'token': 'jwt'})), 'jwt');
|
|
});
|
|
|
|
test('ignores empty and malformed payloads', () {
|
|
expect(parseTelegramAuthMessage(jsonEncode({'token': ''})), isNull);
|
|
expect(parseTelegramAuthMessage('not json'), isNull);
|
|
});
|
|
});
|
|
|
|
group('yandex oauth helpers', () {
|
|
test('builds the authorize URL', () {
|
|
final uri = yandexAuthorizeUri(
|
|
clientId: 'abc',
|
|
redirectUri: 'https://please-pay-me.ru/',
|
|
);
|
|
expect(uri.host, 'oauth.yandex.ru');
|
|
expect(uri.queryParameters['client_id'], 'abc');
|
|
expect(uri.queryParameters['redirect_uri'], 'https://please-pay-me.ru/');
|
|
expect(uri.queryParameters['response_type'], 'code');
|
|
});
|
|
|
|
test('reads a successful cabinet callback on the origin', () {
|
|
final callback = parseYandexCallback(
|
|
Uri.parse('https://please-pay-me.ru/?code=from-yandex'),
|
|
redirectUri: 'https://please-pay-me.ru/',
|
|
);
|
|
expect(callback?.code, 'from-yandex');
|
|
expect(callback?.error, isNull);
|
|
});
|
|
|
|
test('reads a denied callback and ignores other hosts', () {
|
|
expect(
|
|
parseYandexCallback(
|
|
Uri.parse('https://please-pay-me.ru/?error=access_denied'),
|
|
redirectUri: 'https://please-pay-me.ru/',
|
|
)?.error,
|
|
'access_denied',
|
|
);
|
|
expect(
|
|
parseYandexCallback(
|
|
Uri.parse('https://oauth.yandex.ru/authorize?code=nope'),
|
|
redirectUri: 'https://please-pay-me.ru/',
|
|
),
|
|
isNull,
|
|
);
|
|
});
|
|
|
|
test('derives the origin slash that Yandex registered', () {
|
|
expect(
|
|
cabinetYandexRedirectUri('https://please-pay-me.ru/budgets'),
|
|
'https://please-pay-me.ru/',
|
|
);
|
|
expect(
|
|
cabinetYandexRedirectUri(
|
|
'https://please-pay-me.ru',
|
|
configured: 'https://please-pay-me.ru/',
|
|
),
|
|
'https://please-pay-me.ru/',
|
|
);
|
|
});
|
|
});
|
|
|
|
group('SessionController.signInWithToken', () {
|
|
SessionController controllerWith(http.Client client, SessionStorage storage) {
|
|
return SessionController(
|
|
config: AppConfig.demo,
|
|
storage: storage,
|
|
httpClient: client,
|
|
);
|
|
}
|
|
|
|
test('stores the session after the token is validated', () async {
|
|
late Uri requestedUri;
|
|
String? authHeader;
|
|
|
|
final client = MockClient((request) async {
|
|
requestedUri = request.url;
|
|
authHeader = request.headers['Authorization'];
|
|
return http.Response(
|
|
jsonEncode({'user_id': 7, 'username': 'vlad', 'first_name': 'Владимир'}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
});
|
|
|
|
final storage = InMemorySessionStorage();
|
|
final session = controllerWith(client, storage);
|
|
|
|
final ok = await session.signInWithToken(
|
|
baseUrl: 'https://ppm.example.com/',
|
|
token: 'jwt-token',
|
|
);
|
|
|
|
expect(ok, isTrue);
|
|
expect(session.status, SessionStatus.signedIn);
|
|
expect(session.user?.username, 'vlad');
|
|
expect(requestedUri.toString(), 'https://ppm.example.com/api/me');
|
|
expect(authHeader, 'Bearer jwt-token');
|
|
expect((await storage.readAll())['token'], 'jwt-token');
|
|
});
|
|
|
|
test('a rejected token leaves the user signed out with a message', () async {
|
|
final client = MockClient((_) async => http.Response('', 401));
|
|
final session = controllerWith(client, InMemorySessionStorage());
|
|
|
|
final ok = await session.signInWithToken(
|
|
baseUrl: 'https://ppm.example.com',
|
|
token: 'stale',
|
|
);
|
|
|
|
expect(ok, isFalse);
|
|
expect(session.status, SessionStatus.signedOut);
|
|
expect(session.lastError, isNotNull);
|
|
});
|
|
});
|
|
|
|
testWidgets('Yandex login exchanges the code on the API', (tester) async {
|
|
String? seenClientId;
|
|
String? seenRedirect;
|
|
|
|
final client = MockClient((request) async {
|
|
if (request.url.path.endsWith('/api/auth/providers')) {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'yandex': {'enabled': true, 'client_id': 'ya-client'},
|
|
}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
}
|
|
return http.Response(
|
|
jsonEncode({'user_id': 7, 'username': 'ya-user'}),
|
|
200,
|
|
headers: {'content-type': 'application/json'},
|
|
);
|
|
});
|
|
|
|
final session = SessionController(
|
|
config: const AppConfig(
|
|
apiBaseUrl: 'https://ppm.example.com',
|
|
webCabinetUrl: 'https://ppm.example.com',
|
|
demoMode: false,
|
|
),
|
|
storage: InMemorySessionStorage(),
|
|
httpClient: client,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ChangeNotifierProvider<SessionController>.value(
|
|
value: session,
|
|
child: CupertinoApp(
|
|
theme: buildLightTheme(),
|
|
home: LoginScreen(
|
|
launchTelegramLogin: (_, __) async => null,
|
|
launchYandexLogin: (_, {required clientId, required redirectUri}) async {
|
|
seenClientId = clientId;
|
|
seenRedirect = redirectUri;
|
|
return 'jwt-from-yandex';
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
|
|
await tester.ensureVisible(find.byKey(const Key('legal-offer-check')));
|
|
await tester.tap(find.byKey(const Key('legal-offer-check')));
|
|
await tester.pump();
|
|
await tester.ensureVisible(find.byKey(const Key('legal-consent-check')));
|
|
await tester.tap(find.byKey(const Key('legal-consent-check')));
|
|
await tester.pump();
|
|
await tester.tap(find.text('Войти через Яндекс'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
|
|
expect(seenClientId, 'ya-client');
|
|
expect(seenRedirect, 'https://ppm.example.com/');
|
|
expect(session.status, SessionStatus.signedIn);
|
|
expect(session.user?.username, 'ya-user');
|
|
});
|
|
}
|