fix:对齐金值,主要注销,一键登录等方面修改
This commit is contained in:
1 parent
e29ed0a2c8
commit
6997a65c96
37 files changed
+3442
-514
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/auth/data/auth_api.dart';
|
||||
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||
|
||||
void main() {
|
||||
test('keeps the verified one-key login contract', () async {
|
||||
String? requestToken;
|
||||
final dio = Dio()
|
||||
..interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
requestToken = pb.VerifyOneKeyLoginRequest.fromBuffer(
|
||||
options.data as List<int>,
|
||||
).loginToken;
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: pb.VerifySmsCodeReply(
|
||||
accessToken: 'access-token',
|
||||
user: pb.UserInfo(
|
||||
id: 'user-1',
|
||||
phone: '13800138000',
|
||||
createdAt: '2026-09-08T00:00:00Z',
|
||||
),
|
||||
).writeToBuffer(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final result = await AuthApi(dio).verifyOneKeyLogin('umeng-token');
|
||||
|
||||
expect(requestToken, 'umeng-token');
|
||||
expect(result.accessToken, 'access-token');
|
||||
expect(result.user.phone, '13800138000');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart';
|
||||
import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart';
|
||||
import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart';
|
||||
import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart';
|
||||
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
setUp(() => SharedPreferences.setMockInitialValues({}));
|
||||
|
||||
test('restores a token by loading the real profile', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
await fixture.identity.saveJwt('token');
|
||||
|
||||
await fixture.controller.retryBootstrap();
|
||||
|
||||
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||
expect(auth.token, 'token');
|
||||
expect(auth.user.id, 'user-1');
|
||||
expect(auth.user.phone, '13800138000');
|
||||
expect(fixture.preferences.getString(storageUserId), 'user-1');
|
||||
});
|
||||
|
||||
test('deletes the server account before clearing local identity', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
await fixture.identity.saveJwt('token');
|
||||
await fixture.controller.retryBootstrap();
|
||||
|
||||
await fixture.controller.deleteAccount('123456');
|
||||
|
||||
expect(fixture.deletedWithCode, '123456');
|
||||
expect(fixture.controller.state.value, isA<GuestAuthState>());
|
||||
expect(await fixture.identity.readJwt(), isNull);
|
||||
expect(fixture.preferences.getString(storageUserId), isNull);
|
||||
});
|
||||
|
||||
test('keeps the local session when server deletion fails', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
await fixture.identity.saveJwt('token');
|
||||
await fixture.controller.retryBootstrap();
|
||||
fixture.failDeletion = true;
|
||||
|
||||
await expectLater(
|
||||
fixture.controller.deleteAccount('123456'),
|
||||
throwsA(isA<DioException>()),
|
||||
);
|
||||
|
||||
expect(fixture.controller.state.value, isA<LoggedInAuthState>());
|
||||
expect(await fixture.identity.readJwt(), 'token');
|
||||
expect(fixture.preferences.getString(storageUserId), 'user-1');
|
||||
});
|
||||
|
||||
test('clears an unauthorized restored session', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
await fixture.identity.saveJwt('expired');
|
||||
fixture.profileUnauthorized = true;
|
||||
|
||||
await fixture.controller.retryBootstrap();
|
||||
|
||||
expect(fixture.controller.state.value, isA<GuestAuthState>());
|
||||
expect(await fixture.identity.readJwt(), isNull);
|
||||
});
|
||||
|
||||
test('keeps a valid token and cached profile on a network failure', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
await fixture.identity.saveJwt('token');
|
||||
await fixture.preferences.setString('jinzhi_mock_logged_in', '13900139000');
|
||||
fixture.failProfile = true;
|
||||
|
||||
await fixture.controller.retryBootstrap();
|
||||
|
||||
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||
expect(auth.user.id, isEmpty);
|
||||
expect(auth.user.phone, '13900139000');
|
||||
expect(await fixture.identity.readJwt(), 'token');
|
||||
expect(fixture.preferences.getString(storageUserPhone), '13900139000');
|
||||
expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse);
|
||||
});
|
||||
|
||||
test('keeps the verified one-key login state shape', () async {
|
||||
final fixture = await _Fixture.create();
|
||||
|
||||
await fixture.controller.verifyOneKeyLogin('umeng-token');
|
||||
|
||||
final auth = fixture.controller.state.value! as LoggedInAuthState;
|
||||
expect(auth.token, 'one-key-token');
|
||||
expect(auth.user.phone, '13800138000');
|
||||
expect(await fixture.identity.readJwt(), 'one-key-token');
|
||||
});
|
||||
}
|
||||
|
||||
class _Fixture {
|
||||
_Fixture({
|
||||
required this.preferences,
|
||||
required this.identity,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
final SharedPreferences preferences;
|
||||
final SecureIdentityStorage identity;
|
||||
final AuthController controller;
|
||||
bool failDeletion = false;
|
||||
bool failProfile = false;
|
||||
bool profileUnauthorized = false;
|
||||
String? deletedWithCode;
|
||||
|
||||
static Future<_Fixture> create() async {
|
||||
final preferences = await SharedPreferences.getInstance();
|
||||
final identity = SecureIdentityStorage(
|
||||
preferences,
|
||||
secureStorage: _MemorySecureStorage(),
|
||||
);
|
||||
late _Fixture fixture;
|
||||
final dio = Dio()
|
||||
..interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (options.path == '/api/v1/me' && options.method == 'GET') {
|
||||
if (fixture.failProfile) {
|
||||
handler.reject(DioException(requestOptions: options));
|
||||
return;
|
||||
}
|
||||
if (fixture.profileUnauthorized) {
|
||||
handler.reject(
|
||||
DioException(
|
||||
requestOptions: options,
|
||||
response: Response<void>(
|
||||
requestOptions: options,
|
||||
statusCode: 401,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: pb.ProfileReply(
|
||||
user: pb.UserInfo(
|
||||
id: 'user-1',
|
||||
phone: '13800138000',
|
||||
createdAt: '2026-09-08T00:00:00Z',
|
||||
),
|
||||
).writeToBuffer(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.path == '/api/v1/me' && options.method == 'DELETE') {
|
||||
fixture.deletedWithCode = pb.DeleteAccountRequest.fromBuffer(
|
||||
options.data as List<int>,
|
||||
).smsCode;
|
||||
if (fixture.failDeletion) {
|
||||
handler.reject(DioException(requestOptions: options));
|
||||
} else {
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: const <int>[],
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (options.path == '/api/v1/auth/onekey/verify') {
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: pb.VerifySmsCodeReply(
|
||||
accessToken: 'one-key-token',
|
||||
user: pb.UserInfo(phone: '13800138000'),
|
||||
).writeToBuffer(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
final controller = AuthController(
|
||||
dio,
|
||||
identityStorage: identity,
|
||||
preferences: preferences,
|
||||
autoBootstrap: false,
|
||||
);
|
||||
fixture = _Fixture(
|
||||
preferences: preferences,
|
||||
identity: identity,
|
||||
controller: controller,
|
||||
);
|
||||
return fixture;
|
||||
}
|
||||
}
|
||||
|
||||
class _MemorySecureStorage implements SecureKeyValueStorage {
|
||||
final Map<String, String> _values = {};
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) async => _values.remove(key);
|
||||
|
||||
@override
|
||||
Future<String?> read(String key) async => _values[key];
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value) async {
|
||||
_values[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,14 @@ import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dar
|
||||
void main() {
|
||||
test('preserves Jinzhi attribution while syncing H5 login', () {
|
||||
final arguments = buildEnterWebMallBridgeArguments(
|
||||
config: const MallTabConfig(
|
||||
enabled: true,
|
||||
url: 'https://mall.example.com/',
|
||||
adId: 'latest-ad',
|
||||
trackingId: 'latest-tracking',
|
||||
clickId: 'latest-click',
|
||||
entryRequest: MallEntryRequest.quoteRetailGold,
|
||||
),
|
||||
currentUri: Uri.parse(
|
||||
'https://mall.example.com/?from=jinzhi&source_app=jinzhi'
|
||||
'&source_entry=market_quote&placement=mall_tab&hook_type=quote_cta'
|
||||
@@ -15,11 +23,6 @@ void main() {
|
||||
authToken: 'mall-token',
|
||||
scene: 'web_mall',
|
||||
sourceApp: 'jinzhi',
|
||||
attribution: const MallClickAttribution(
|
||||
adId: 'latest-ad',
|
||||
trackingId: 'latest-tracking',
|
||||
clickId: 'latest-click',
|
||||
),
|
||||
);
|
||||
|
||||
expect(arguments['from'], 'jinzhi');
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||
|
||||
void main() {
|
||||
test('keeps click attribution and reports the full generated event', () async {
|
||||
pb.ReportAdEventRequest? clickEvent;
|
||||
final dio = Dio()
|
||||
..interceptors.add(
|
||||
InterceptorsWrapper(
|
||||
onRequest: (options, handler) {
|
||||
if (options.path == '/api/v1/ads') {
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: pb.GetAdReply(
|
||||
enabled: true,
|
||||
ad: pb.AdCreative(
|
||||
adId: 'ad-1',
|
||||
h5Url: 'https://mall.example.com',
|
||||
trackingId: 'tracking-1',
|
||||
openMode: pb.AdOpenMode.AD_OPEN_MODE_IN_APP,
|
||||
),
|
||||
).writeToBuffer(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (options.path == '/api/v1/ads/events') {
|
||||
final event = pb.ReportAdEventRequest.fromBuffer(
|
||||
options.data as List<int>,
|
||||
);
|
||||
if (event.eventName == 'ad_click') clickEvent = event;
|
||||
handler.resolve(
|
||||
Response<List<int>>(
|
||||
requestOptions: options,
|
||||
statusCode: 200,
|
||||
data: pb.ReportAdEventReply().writeToBuffer(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
),
|
||||
);
|
||||
final controller = MallTabConfigController(dio, autoRefresh: false);
|
||||
await controller.refresh();
|
||||
|
||||
final clicked = await controller.recordClick(
|
||||
request: MallEntryRequest.quoteRetailGold,
|
||||
);
|
||||
|
||||
expect(clicked.clickId, isNotEmpty);
|
||||
expect(controller.state.clickId, clicked.clickId);
|
||||
expect(clickEvent?.clickId, clicked.clickId);
|
||||
expect(clickEvent?.attrs['source_app'], 'jinzhi');
|
||||
expect(clickEvent?.attrs['source_entry'], 'market_quote');
|
||||
expect(clickEvent?.attrs['hook_type'], 'quote_cta');
|
||||
expect(clickEvent?.attrs['entry_scene'], 'retail');
|
||||
expect(clickEvent?.attrs['entry_metal'], 'au');
|
||||
expect(clickEvent?.attrs['open_mode'], 'in_app');
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/mall/application/mall_tab_config.dart';
|
||||
import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/ads.pb.dart' as pb;
|
||||
|
||||
void main() {
|
||||
test('builds the resident mall URL with Jinzhi attribution and theme', () {
|
||||
@@ -102,6 +103,27 @@ void main() {
|
||||
expect(config.entryTipTitle, '新人礼:¥360 铂金订购券');
|
||||
expect(config.entryTipBody, '点击这里进商城');
|
||||
});
|
||||
|
||||
test('parses generated mall promotion and open-mode fields', () {
|
||||
final response = pb.GetAdReply(
|
||||
enabled: true,
|
||||
ad: pb.AdCreative(
|
||||
adId: 'ad-1',
|
||||
title: '会员商城',
|
||||
h5Url: 'https://mall.example.com',
|
||||
trackingId: 'tracking-1',
|
||||
openMode: pb.AdOpenMode.AD_OPEN_MODE_NATIVE_APP,
|
||||
mallPromotion: pb.MallPromotion(quoteCtaEnabled: true),
|
||||
),
|
||||
);
|
||||
|
||||
final config = parseMallTabConfigResponse(response.writeToBuffer());
|
||||
|
||||
expect(config.isVisible, isTrue);
|
||||
expect(config.openMode, 'native_app');
|
||||
expect(config.quoteCtaEnabled, isTrue);
|
||||
expect(config.allowedHosts, {'mall.example.com'});
|
||||
});
|
||||
}
|
||||
|
||||
List<int> _stringField(int tag, String value) =>
|
||||
|
||||
@@ -2,13 +2,14 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jinzhi_flutter_app/features/web/presentation/web_page.dart';
|
||||
|
||||
void main() {
|
||||
test('accepts only http and https URLs with a host', () {
|
||||
test('accepts HTTPS and rejects unsafe or remote HTTP URLs', () {
|
||||
expect(
|
||||
parseSafeWebUri('https://h5.example.com/page')?.host,
|
||||
'h5.example.com',
|
||||
);
|
||||
expect(parseSafeWebUri('mailto:test@example.com'), isNull);
|
||||
expect(parseSafeWebUri('javascript:alert(1)'), isNull);
|
||||
expect(parseSafeWebUri('http://example.com/page'), isNull);
|
||||
expect(parseSafeWebUri(''), isNull);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user