211 lines
7.9 KiB
Dart
211 lines
7.9 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:drift/native.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart' show RenderParagraph;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:mews/application/key_store.dart';
|
|
import 'package:mews/database/database.dart';
|
|
import 'package:mews/l10n/strings.dart';
|
|
import 'package:mews/presentation/app.dart';
|
|
|
|
import 'fake_fetch.dart';
|
|
|
|
/// A PEM holding both the certificate and its key, the way a Gemini identity
|
|
/// commonly travels.
|
|
String combinedPem() =>
|
|
'${File('test/fixtures/client-test-cert.pem').readAsStringSync()}\n'
|
|
'${File('test/fixtures/client-test-key.pem').readAsStringSync()}';
|
|
|
|
/// Taps the run reading [label] inside a rendered paragraph; internal pages
|
|
/// put several links in one paragraph, so tapping the widget's center would
|
|
/// hit whichever run happens to sit in the middle.
|
|
Future<void> tapRun(WidgetTester tester, String label) async {
|
|
final paragraph = tester.renderObject<RenderParagraph>(
|
|
find
|
|
.descendant(
|
|
of: find.byWidgetPredicate((widget) =>
|
|
widget is Text &&
|
|
(widget.textSpan?.toPlainText() ?? widget.data ?? '')
|
|
.contains(label)),
|
|
matching: find.byType(RichText),
|
|
)
|
|
.first,
|
|
);
|
|
final plain = paragraph.text.toPlainText();
|
|
final start = plain.indexOf(label);
|
|
final box = paragraph
|
|
.getBoxesForSelection(
|
|
TextSelection(baseOffset: start, extentOffset: start + label.length))
|
|
.first;
|
|
await tester.tapAt(
|
|
paragraph.localToGlobal(Offset(box.left + 2, (box.top + box.bottom) / 2)));
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
void main() {
|
|
late MewsDatabase db;
|
|
late Directory keyDir;
|
|
late Strings strings;
|
|
late AppDependencies deps;
|
|
|
|
setUp(() async {
|
|
db = MewsDatabase.test(NativeDatabase.memory());
|
|
keyDir = await Directory.systemTemp.createTemp('mews-keys');
|
|
strings = Strings();
|
|
strings.loadSource(File('locales/en-US/main.ftl').readAsStringSync());
|
|
deps = AppDependencies(
|
|
database: db,
|
|
fetch: FakeFetch(const {}),
|
|
strings: strings,
|
|
keys: KeyStore(() async => keyDir),
|
|
pickPemFile: (title) async => (name: 'reader', content: combinedPem()),
|
|
);
|
|
});
|
|
|
|
tearDown(() async {
|
|
await db.close();
|
|
keyDir.deleteSync(recursive: true);
|
|
});
|
|
|
|
Future<void> pumpCertificates(WidgetTester tester) async {
|
|
await tester.pumpWidget(MewsApp(deps: deps, strings: strings));
|
|
await tester.pumpAndSettle();
|
|
await deps.browser.open('mews:certificates');
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
testWidgets('a certificate is imported, renamed and removed in a second step',
|
|
(tester) async {
|
|
await pumpCertificates(tester);
|
|
expect(find.textContaining('Certificates you add'), findsOneWidget);
|
|
|
|
// The pick is injected; its file name names the identity. The name shows
|
|
// in the row and prefills the rename form under it.
|
|
await tapRun(tester, 'Add a certificate');
|
|
expect(find.textContaining('reader'), findsWidgets);
|
|
expect(await db.allClientCertificates(), hasLength(1));
|
|
expect(await deps.keys.read(1), isNotEmpty);
|
|
// The address bar is a TextField too, so the form is the last one.
|
|
expect(find.byType(TextField), findsNWidgets(2));
|
|
expect(tester.widget<TextField>(find.byType(TextField).last).controller!.text,
|
|
'reader');
|
|
|
|
// Renaming happens in place, on the form under the identity's row.
|
|
await tester.enterText(find.byType(TextField).last, 'Reader');
|
|
await tester.tap(find.text('Save name'));
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('Reader'), findsWidgets);
|
|
expect((await db.allClientCertificates()).single.name, 'Reader');
|
|
|
|
// Removing asks first, the way every irreversible act here does.
|
|
await tapRun(tester, 'Remove');
|
|
expect(find.textContaining('stops presenting'), findsOneWidget);
|
|
await tapRun(tester, 'Keep it');
|
|
await tester.pumpAndSettle();
|
|
expect((await db.allClientCertificates()), hasLength(1));
|
|
|
|
await tapRun(tester, 'Remove');
|
|
await tapRun(tester, 'Remove the certificate');
|
|
expect(find.textContaining('Certificates you add'), findsOneWidget);
|
|
expect(await db.allClientCertificates(), isEmpty);
|
|
expect(await deps.keys.read(1), isNull);
|
|
});
|
|
|
|
testWidgets('a cancelled pick imports nothing', (tester) async {
|
|
final cancelledDeps = AppDependencies(
|
|
database: db,
|
|
fetch: FakeFetch(const {}),
|
|
strings: strings,
|
|
keys: KeyStore(() async => keyDir),
|
|
pickPemFile: (title) async => null,
|
|
);
|
|
await tester.pumpWidget(MewsApp(deps: cancelledDeps, strings: strings));
|
|
await tester.pumpAndSettle();
|
|
await cancelledDeps.browser.open('mews:certificates');
|
|
await tester.pumpAndSettle();
|
|
|
|
await tapRun(tester, 'Add a certificate');
|
|
|
|
expect(find.textContaining('Certificates you add'), findsOneWidget);
|
|
expect(await db.allClientCertificates(), isEmpty);
|
|
});
|
|
|
|
testWidgets('a key picked alone asks for its certificate', (tester) async {
|
|
final picks = <String>[];
|
|
final keyOnly = AppDependencies(
|
|
database: db,
|
|
fetch: FakeFetch(const {}),
|
|
strings: strings,
|
|
keys: KeyStore(() async => keyDir),
|
|
pickPemFile: (title) async {
|
|
picks.add(title);
|
|
if (picks.length == 1) {
|
|
// A .key file alone: the key, but no certificate to wear it in.
|
|
return (name: 'reader-key', content: File('test/fixtures/client-test-key.pem').readAsStringSync());
|
|
}
|
|
return (
|
|
name: 'reader-cert',
|
|
content: File('test/fixtures/client-test-cert.pem').readAsStringSync()
|
|
);
|
|
},
|
|
);
|
|
await tester.pumpWidget(MewsApp(deps: keyOnly, strings: strings));
|
|
await tester.pumpAndSettle();
|
|
await keyOnly.browser.open('mews:certificates');
|
|
await tester.pumpAndSettle();
|
|
|
|
await tapRun(tester, 'Add a certificate');
|
|
|
|
// The second pick was asked for by name, so the key was not silently
|
|
// dropped, and the pair imported together.
|
|
expect(picks, ['Choose a certificate', 'Choose the certificate that goes with the key']);
|
|
expect(await db.allClientCertificates(), hasLength(1));
|
|
expect(await keyOnly.keys.read(1), isNotEmpty);
|
|
expect(find.textContaining('reader-key'), findsWidgets);
|
|
});
|
|
|
|
testWidgets('a key picked alone with nothing after it imports nothing',
|
|
(tester) async {
|
|
final cancelled = AppDependencies(
|
|
database: db,
|
|
fetch: FakeFetch(const {}),
|
|
strings: strings,
|
|
keys: KeyStore(() async => keyDir),
|
|
pickPemFile: (title) async => title.contains('goes with')
|
|
? null
|
|
: (name: 'reader-key', content: File('test/fixtures/client-test-key.pem').readAsStringSync()),
|
|
);
|
|
await tester.pumpWidget(MewsApp(deps: cancelled, strings: strings));
|
|
await tester.pumpAndSettle();
|
|
await cancelled.browser.open('mews:certificates');
|
|
await tester.pumpAndSettle();
|
|
|
|
await tapRun(tester, 'Add a certificate');
|
|
|
|
// A key alone cannot be worn, and cancelling the second pick keeps
|
|
// nothing of it.
|
|
expect(await db.allClientCertificates(), isEmpty);
|
|
expect(await cancelled.keys.read(1), isNull);
|
|
expect(find.textContaining('Certificates you add'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('a certificate is presented only to the sites that ask for it',
|
|
(tester) async {
|
|
final id = await db.addClientCertificate('Reader', 'CERT PEM');
|
|
await db.bindCertificate('capsule.example', 1965, id);
|
|
await pumpCertificates(tester);
|
|
|
|
// The row names the site the identity is worn at.
|
|
expect(find.textContaining('capsule.example:1965'), findsOneWidget);
|
|
|
|
// Removing the identity takes the binding with it, so the site is left
|
|
// asking rather than pointing at a certificate that is no longer there.
|
|
await db.deleteClientCertificate(id);
|
|
await deps.browser.reload();
|
|
await tester.pumpAndSettle();
|
|
expect(find.textContaining('Certificates you add'), findsOneWidget);
|
|
expect(await db.bindingFor('capsule.example', 1965), isNull);
|
|
});
|
|
}
|