feat(proj): init
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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');
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user