Add Baron Safe app features through session block skeleton
ci / flutter-check (push) Successful in 4s
ci / flutter-check (push) Successful in 4s
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_message_handler.dart';
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('BaronSafeBridgeMessageHandler', () {
|
||||
const handler = BaronSafeBridgeMessageHandler(
|
||||
PlaceholderBaronSafeBridgeService(),
|
||||
);
|
||||
|
||||
test('handles an allowed command and returns response envelope', () async {
|
||||
final rawResponse = await handler.handle(
|
||||
jsonEncode({'requestId': 'req-1', 'command': 'getDeviceInfo'}),
|
||||
);
|
||||
|
||||
final response = jsonDecode(rawResponse) as Map<String, Object?>;
|
||||
|
||||
expect(response['requestId'], 'req-1');
|
||||
expect(response['ok'], isTrue);
|
||||
expect(response['data'], isA<Map<String, Object?>>());
|
||||
});
|
||||
|
||||
test('rejects unknown commands before service handling', () async {
|
||||
final rawResponse = await handler.handle(
|
||||
jsonEncode({'requestId': 'req-2', 'command': 'stealPrivateKey'}),
|
||||
);
|
||||
|
||||
final response = jsonDecode(rawResponse) as Map<String, Object?>;
|
||||
final error = response['error'] as Map<String, Object?>;
|
||||
|
||||
expect(response['ok'], isFalse);
|
||||
expect(error['code'], 'unsupported_command');
|
||||
});
|
||||
|
||||
test('rejects invalid JSON messages', () async {
|
||||
final rawResponse = await handler.handle('not-json');
|
||||
|
||||
final response = jsonDecode(rawResponse) as Map<String, Object?>;
|
||||
final error = response['error'] as Map<String, Object?>;
|
||||
|
||||
expect(response['ok'], isFalse);
|
||||
expect(error['code'], 'invalid_json');
|
||||
});
|
||||
|
||||
test('rejects non-object payloads', () async {
|
||||
final rawResponse = await handler.handle(
|
||||
jsonEncode({
|
||||
'requestId': 'req-3',
|
||||
'command': 'openBiometricPrompt',
|
||||
'payload': 'bad-payload',
|
||||
}),
|
||||
);
|
||||
|
||||
final response = jsonDecode(rawResponse) as Map<String, Object?>;
|
||||
final error = response['error'] as Map<String, Object?>;
|
||||
|
||||
expect(response['ok'], isFalse);
|
||||
expect(error['code'], 'invalid_payload');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_command.dart';
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_models.dart';
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_service.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_block_service.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_models.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@@ -77,5 +79,55 @@ void main() {
|
||||
expect(response.isSuccess, isFalse);
|
||||
expect(response.error?.code, 'not_implemented');
|
||||
});
|
||||
|
||||
test('rejects blockSession when sessionId is missing', () async {
|
||||
const service = PlaceholderBaronSafeBridgeService(
|
||||
sessionBlockService: _FakeSessionBlockService(),
|
||||
);
|
||||
|
||||
final response = await service.handle(
|
||||
const BaronSafeBridgeRequest(
|
||||
command: BaronSafeBridgeCommand.blockSession,
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.isSuccess, isFalse);
|
||||
expect(response.error?.code, 'invalid_payload');
|
||||
});
|
||||
|
||||
test('runs injected session block service', () async {
|
||||
const service = PlaceholderBaronSafeBridgeService(
|
||||
sessionBlockService: _FakeSessionBlockService(),
|
||||
);
|
||||
|
||||
final response = await service.handle(
|
||||
const BaronSafeBridgeRequest(
|
||||
command: BaronSafeBridgeCommand.blockSession,
|
||||
payload: {'sessionId': 'session-1', 'reason': 'Suspicious login'},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.isSuccess, isTrue);
|
||||
expect(response.data['sessionId'], 'session-1');
|
||||
expect(response.data['blocked'], isTrue);
|
||||
expect(response.data['biometricVerified'], isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeSessionBlockService implements BaronSafeSessionBlockService {
|
||||
const _FakeSessionBlockService();
|
||||
|
||||
@override
|
||||
Future<BaronSafeSessionBlockResult> blockSession({
|
||||
required String sessionId,
|
||||
required String reason,
|
||||
}) async {
|
||||
return BaronSafeSessionBlockResult(
|
||||
sessionId: sessionId,
|
||||
blocked: true,
|
||||
biometricVerified: true,
|
||||
message: reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'package:baron_safe_app/src/features/device/baron_safe_device_models.dart';
|
||||
import 'package:baron_safe_app/src/features/device/baron_safe_device_registration_client.dart';
|
||||
import 'package:baron_safe_app/src/features/device/baron_safe_device_registration_service.dart';
|
||||
import 'package:baron_safe_app/src/features/push/baron_safe_push_models.dart';
|
||||
import 'package:baron_safe_app/src/features/push/baron_safe_push_service.dart';
|
||||
import 'package:baron_safe_app/src/features/secure_storage/baron_safe_secure_storage.dart';
|
||||
import 'package:baron_safe_app/src/features/secure_storage/baron_safe_secure_storage_service.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('BaronSafeDeviceRegistrationService', () {
|
||||
test('creates, stores, and registers a new device id', () async {
|
||||
final secureStorage = DefaultBaronSafeSecureStorageService(
|
||||
MemoryBaronSafeKeyValueStore(),
|
||||
);
|
||||
final client = _RecordingDeviceRegistrationClient();
|
||||
final service = BaronSafeDeviceRegistrationService(
|
||||
secureStorage: secureStorage,
|
||||
registrationClient: client,
|
||||
pushTokenProvider: const _FixedPushTokenProvider(
|
||||
BaronSafePushToken(
|
||||
token: 'push-token-1',
|
||||
platform: BaronSafePushPlatform.android,
|
||||
deviceId: 'ignored-device-id',
|
||||
appVersion: '0.1.0',
|
||||
),
|
||||
),
|
||||
deviceIdGenerator: () => 'device-1',
|
||||
);
|
||||
|
||||
final result = await service.registerCurrentDevice();
|
||||
|
||||
expect(result.registered, isTrue);
|
||||
expect(result.deviceId, 'device-1');
|
||||
expect(await secureStorage.readDeviceId(), 'device-1');
|
||||
expect(client.lastRequest?.pushToken, 'push-token-1');
|
||||
expect(client.lastRequest?.platform, BaronSafePushPlatform.android);
|
||||
});
|
||||
|
||||
test('reuses an existing device id', () async {
|
||||
final secureStorage = DefaultBaronSafeSecureStorageService(
|
||||
MemoryBaronSafeKeyValueStore(),
|
||||
);
|
||||
await secureStorage.saveDeviceId('existing-device');
|
||||
final client = _RecordingDeviceRegistrationClient();
|
||||
final service = BaronSafeDeviceRegistrationService(
|
||||
secureStorage: secureStorage,
|
||||
registrationClient: client,
|
||||
pushTokenProvider: const _FixedPushTokenProvider(null),
|
||||
deviceIdGenerator: () => 'new-device',
|
||||
);
|
||||
|
||||
final result = await service.registerCurrentDevice();
|
||||
|
||||
expect(result.deviceId, 'existing-device');
|
||||
expect(client.lastRequest?.deviceId, 'existing-device');
|
||||
expect(client.lastRequest?.pushToken, isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _RecordingDeviceRegistrationClient
|
||||
implements BaronSafeDeviceRegistrationClient {
|
||||
BaronSafeDeviceRegistrationRequest? lastRequest;
|
||||
|
||||
@override
|
||||
Future<BaronSafeDeviceRegistrationResult> registerDevice(
|
||||
BaronSafeDeviceRegistrationRequest request,
|
||||
) async {
|
||||
lastRequest = request;
|
||||
return BaronSafeDeviceRegistrationResult(
|
||||
deviceId: request.deviceId,
|
||||
registered: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FixedPushTokenProvider implements BaronSafePushTokenProvider {
|
||||
const _FixedPushTokenProvider(this._token);
|
||||
|
||||
final BaronSafePushToken? _token;
|
||||
|
||||
@override
|
||||
Future<BaronSafePushToken?> readCurrentToken() async {
|
||||
return _token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_models.dart';
|
||||
import 'package:baron_safe_app/src/features/bridge/baron_safe_bridge_service.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_block_service.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_client.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_models.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('BiometricBaronSafeSessionBlockService', () {
|
||||
test('blocks a session after biometric verification succeeds', () async {
|
||||
final repository = ClientBaronSafeSessionRepository(
|
||||
MockBaronSafeSessionClient(),
|
||||
);
|
||||
final service = BiometricBaronSafeSessionBlockService(
|
||||
repository: repository,
|
||||
biometricPrompt: const _FakeBiometricPrompt(verified: true),
|
||||
);
|
||||
|
||||
final result = await service.blockSession(
|
||||
sessionId: 'session-1',
|
||||
reason: 'Suspicious login',
|
||||
);
|
||||
|
||||
expect(result.sessionId, 'session-1');
|
||||
expect(result.biometricVerified, isTrue);
|
||||
expect(result.blocked, isTrue);
|
||||
expect(result.message, 'Session blocked');
|
||||
});
|
||||
|
||||
test('does not call block API when biometric verification fails', () async {
|
||||
final repository = _RecordingSessionRepository();
|
||||
final service = BiometricBaronSafeSessionBlockService(
|
||||
repository: repository,
|
||||
biometricPrompt: const _FakeBiometricPrompt(
|
||||
verified: false,
|
||||
reason: 'User canceled biometric prompt',
|
||||
),
|
||||
);
|
||||
|
||||
final result = await service.blockSession(
|
||||
sessionId: 'session-1',
|
||||
reason: 'Suspicious login',
|
||||
);
|
||||
|
||||
expect(result.biometricVerified, isFalse);
|
||||
expect(result.blocked, isFalse);
|
||||
expect(result.message, 'User canceled biometric prompt');
|
||||
expect(repository.blockCallCount, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeBiometricPrompt implements BaronSafeBiometricPrompt {
|
||||
const _FakeBiometricPrompt({required this.verified, this.reason = 'OK'});
|
||||
|
||||
final bool verified;
|
||||
final String reason;
|
||||
|
||||
@override
|
||||
Future<BaronSafeBiometricResult> verify({required String reason}) async {
|
||||
return BaronSafeBiometricResult(verified: verified, reason: this.reason);
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingSessionRepository implements BaronSafeSessionRepository {
|
||||
int blockCallCount = 0;
|
||||
|
||||
@override
|
||||
Future<BaronSafeSessionBlockResponse> blockSession(
|
||||
BaronSafeSessionBlockRequest request,
|
||||
) async {
|
||||
blockCallCount += 1;
|
||||
return BaronSafeSessionBlockResponse(
|
||||
sessionId: request.sessionId,
|
||||
blocked: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<BaronSafeSessionDetail> getSessionDetail(String sessionId) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<BaronSafeSessionSummary>> listRecentSessions() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_client.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_models.dart';
|
||||
import 'package:baron_safe_app/src/features/session/baron_safe_session_repository.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('ClientBaronSafeSessionRepository', () {
|
||||
test('lists recent sessions from the client', () async {
|
||||
final repository = ClientBaronSafeSessionRepository(
|
||||
MockBaronSafeSessionClient(),
|
||||
);
|
||||
|
||||
final sessions = await repository.listRecentSessions();
|
||||
|
||||
expect(sessions, hasLength(2));
|
||||
expect(sessions.first.id, 'session-1');
|
||||
expect(sessions.last.riskLevel, BaronSafeSessionRiskLevel.high);
|
||||
});
|
||||
|
||||
test('loads a session detail by id', () async {
|
||||
final repository = ClientBaronSafeSessionRepository(
|
||||
MockBaronSafeSessionClient(),
|
||||
);
|
||||
|
||||
final session = await repository.getSessionDetail('session-2');
|
||||
|
||||
expect(session.serviceName, 'Admin Console');
|
||||
expect(session.locationLabel, 'Unknown');
|
||||
expect(session.status, BaronSafeSessionStatus.active);
|
||||
});
|
||||
|
||||
test('throws when a session id is unknown', () async {
|
||||
final repository = ClientBaronSafeSessionRepository(
|
||||
MockBaronSafeSessionClient(),
|
||||
);
|
||||
|
||||
expect(
|
||||
() => repository.getSessionDetail('missing-session'),
|
||||
throwsA(isA<BaronSafeSessionNotFoundException>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('blocks a known session through the client', () async {
|
||||
final repository = ClientBaronSafeSessionRepository(
|
||||
MockBaronSafeSessionClient(),
|
||||
);
|
||||
|
||||
final result = await repository.blockSession(
|
||||
const BaronSafeSessionBlockRequest(
|
||||
sessionId: 'session-1',
|
||||
reason: 'Suspicious login',
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.sessionId, 'session-1');
|
||||
expect(result.blocked, isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('BaronSafeSessionDetail', () {
|
||||
test('round trips from JSON to model', () {
|
||||
final session = BaronSafeSessionDetail.fromJson({
|
||||
'id': 'session-json',
|
||||
'serviceName': 'JSON Service',
|
||||
'status': 'blocked',
|
||||
'riskLevel': 'medium',
|
||||
'createdAt': '2026-06-30T10:00:00.000Z',
|
||||
'ipAddress': '203.0.113.11',
|
||||
'deviceLabel': 'Safari on iOS',
|
||||
'locationLabel': 'Busan',
|
||||
'userAgent': 'Safari',
|
||||
'authMethod': 'phone',
|
||||
});
|
||||
|
||||
expect(session.status, BaronSafeSessionStatus.blocked);
|
||||
expect(session.riskLevel, BaronSafeSessionRiskLevel.medium);
|
||||
expect(session.toJson()['locationLabel'], 'Busan');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:baron_safe_app/src/features/webview/baron_safe_webview_policy.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('BaronSafeWebViewPolicy', () {
|
||||
const policy = BaronSafeWebViewPolicy();
|
||||
|
||||
test('allows production and staging HTTPS hosts', () {
|
||||
expect(policy.allows(Uri.parse('https://safe.baron.hmac.kr')), isTrue);
|
||||
expect(
|
||||
policy.allows(Uri.parse('https://safe-staging.baron.hmac.kr/path')),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('allows local HTTP development hosts', () {
|
||||
expect(policy.allows(Uri.parse('http://localhost:8080')), isTrue);
|
||||
expect(policy.allows(Uri.parse('http://127.0.0.1:8080')), isTrue);
|
||||
expect(policy.allows(Uri.parse('http://10.0.2.2:8080')), isTrue);
|
||||
});
|
||||
|
||||
test('blocks unknown hosts and insecure remote HTTP', () {
|
||||
expect(policy.allows(Uri.parse('https://example.com')), isFalse);
|
||||
expect(policy.allows(Uri.parse('http://safe.baron.hmac.kr')), isFalse);
|
||||
});
|
||||
|
||||
test('blocks non-web schemes and relative URLs', () {
|
||||
expect(policy.allows(Uri.parse('javascript:alert(1)')), isFalse);
|
||||
expect(policy.allows(Uri.parse('/relative/path')), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user