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.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'); }); }