Files
please-pay-me/mobile/test/auth_test.dart
T
2026-09-21 04:06:43 +03:00

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