Files
sharedinbox/test/widget/account_export_screen_test.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 04e65d2fba feat: secure account sharing via public-key encryption (#107)
Replace the insecure plaintext QR export/import flow with an
end-to-end-encrypted account-transfer mechanism:

- Receiver generates an ephemeral X25519 key pair (20-minute lifetime,
  stored in the new share_keys DB table at schema v31) and displays it
  as a QR code (sharedinbox.de:pubkey:v1:…).
- Sender scans the public-key QR, selects accounts (or auto-selects
  when only one exists), encrypts them with ECIES (X25519-ECDH +
  HKDF-SHA256 + AES-256-GCM) and displays an encrypted QR
  (sharedinbox.de:encrypted-accounts:v1:…).
- Receiver scans the encrypted QR, decrypts, verifies the 20-minute
  expiry and MAC authentication tag, then imports the accounts.

New screens: AccountReceiveScreen (/accounts/receive) and
AccountSendScreen (/accounts/send), accessible from the account-list
drawer and per-account popup menu respectively.

Remove the old insecure AccountExportScreen and AccountImportScreen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 01:19:01 +02:00

156 lines
4.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/core/services/share_encryption_service.dart';
import 'helpers.dart';
void main() {
group('AccountReceiveScreen', () {
testWidgets('shows pubkey QR code and scan button after key generation', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/receive',
overrides: baseOverrides(),
),
);
// Allow async key generation to complete.
await tester.pumpAndSettle();
expect(find.byKey(const Key('pubKeyQrCode')), findsOneWidget);
expect(find.byKey(const Key('scanEncryptedButton')), findsOneWidget);
});
testWidgets('shows 20-minute expiry hint', (tester) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/receive',
overrides: baseOverrides(),
),
);
await tester.pumpAndSettle();
expect(find.textContaining('20 minutes'), findsOneWidget);
});
});
group('AccountSendScreen', () {
testWidgets('shows camera scanner (or text fallback) on load', (
tester,
) async {
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/send',
overrides: baseOverrides(
accounts: [
const Account(
id: 'acc-1',
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
),
],
),
),
);
await tester.pumpAndSettle();
// On Linux (desktop without camera), the text-fallback field appears.
// On mobile, the MobileScanner widget would be shown.
// Either way the screen renders without crash.
expect(find.byType(Scaffold), findsAtLeastNWidgets(1));
});
testWidgets('shows account selection when multiple accounts present', (
tester,
) async {
const account1 = Account(
id: 'acc-1',
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
);
const account2 = Account(
id: 'acc-2',
displayName: 'Bob',
email: 'bob@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
);
// Generate a real key pair and a valid pubkey QR string to feed in.
final material = await ShareEncryptionService.generateKeyPair();
final pubKeyQr = ShareEncryptionService.encodePublicKeyQr(
material.keyId,
material.publicKeyBytes,
);
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/send',
overrides: baseOverrides(accounts: [account1, account2]),
),
);
await tester.pumpAndSettle();
// On desktop the text fallback is shown — simulate pasting the pubkey.
final field = find.byKey(const Key('pubKeyInputField'));
if (field.evaluate().isNotEmpty) {
await tester.enterText(field, pubKeyQr);
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// With two accounts the selection list should appear.
expect(find.byKey(const Key('sendSelectedButton')), findsOneWidget);
expect(find.text('Alice'), findsOneWidget);
expect(find.text('Bob'), findsOneWidget);
}
// On mobile the MobileScanner handles this; we skip it in widget tests.
});
testWidgets('shows encrypted QR after single account auto-select', (
tester,
) async {
const account = Account(
id: 'acc-1',
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
smtpHost: 'smtp.example.com',
);
final material = await ShareEncryptionService.generateKeyPair();
final pubKeyQr = ShareEncryptionService.encodePublicKeyQr(
material.keyId,
material.publicKeyBytes,
);
await tester.pumpWidget(
buildApp(
initialLocation: '/accounts/send',
overrides: baseOverrides(accounts: [account]),
),
);
await tester.pumpAndSettle();
final field = find.byKey(const Key('pubKeyInputField'));
if (field.evaluate().isNotEmpty) {
await tester.enterText(field, pubKeyQr);
await tester.tap(find.text('Continue'));
await tester.pumpAndSettle();
// Single account → auto-selected → encrypted QR shown immediately.
expect(
find.byKey(const Key('encryptedAccountsQrCode')),
findsOneWidget,
);
expect(find.byKey(const Key('copyEncryptedButton')), findsOneWidget);
}
});
});
}