feat: add Gemini and Spartan protocol support
This commit is contained in:
parent
a4a49f41d7
commit
fa7aa5557c
48 changed files with 4992 additions and 161 deletions
|
|
@ -1,7 +1,14 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:actors/actors.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:mews/application/actors/fetch_actor.dart';
|
||||
import 'package:mews/mews/document.dart';
|
||||
|
||||
import 'fake_protocol_server.dart';
|
||||
|
||||
const page = '''
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
|
@ -11,6 +18,8 @@ const page = '''
|
|||
</html>
|
||||
''';
|
||||
|
||||
const gemtext = '# Title\n\nSome text.\n';
|
||||
|
||||
void main() {
|
||||
test('ParseSource parses without touching the network', () async {
|
||||
final actor = Actor.create(FetchHandler.new);
|
||||
|
|
@ -36,4 +45,166 @@ void main() {
|
|||
expect(result, isA<FetchParseError>());
|
||||
expect((result as FetchParseError).source, '<p>nope');
|
||||
});
|
||||
|
||||
// The protocol transports are exercised through the same handler a fetch
|
||||
// uses, against in-process servers, so these tests cover the whole path
|
||||
// from command to page.
|
||||
group('protocol dispatch', () {
|
||||
final handler = FetchHandler();
|
||||
StreamSubscription<Socket>? spartanAnswers;
|
||||
StreamSubscription<SecureSocket>? geminiAnswers;
|
||||
FakeSpartanServer? spartan;
|
||||
FakeGeminiServer? geminiServer;
|
||||
|
||||
tearDown(() async {
|
||||
await spartanAnswers?.cancel();
|
||||
await geminiAnswers?.cancel();
|
||||
await spartan?.close();
|
||||
await geminiServer?.close();
|
||||
});
|
||||
|
||||
test('a spartan page becomes a gemtext page', () async {
|
||||
final server = FakeSpartanServer();
|
||||
await server.start();
|
||||
spartan = server;
|
||||
spartanAnswers = server
|
||||
.answer((line, upload, socket) => socket.add(utf8.encode('2 text/gemini\r\n$gemtext')));
|
||||
|
||||
final result = await handler.handle(FetchPage(url: 'spartan://localhost:${server.port}/'));
|
||||
|
||||
expect(result, isA<FetchPageOk>());
|
||||
final ok = result as FetchPageOk;
|
||||
expect(ok.page.title, 'Title');
|
||||
expect(ok.page.isMews, isTrue);
|
||||
expect(ok.page.body.first, isA<HeadingBlock>());
|
||||
expect(ok.source, gemtext);
|
||||
});
|
||||
|
||||
test('a spartan error status carries its message', () async {
|
||||
final server = FakeSpartanServer();
|
||||
await server.start();
|
||||
spartan = server;
|
||||
spartanAnswers = server
|
||||
.answer((line, upload, socket) => socket.add(utf8.encode('4 Nothing lives here\r\n')));
|
||||
|
||||
final result =
|
||||
await handler.handle(FetchPage(url: 'spartan://localhost:${server.port}/gone'));
|
||||
|
||||
expect(result, isA<FetchProtocolError>());
|
||||
expect((result as FetchProtocolError).message, 'Nothing lives here');
|
||||
});
|
||||
|
||||
test('a gemini input request becomes a one-field form', () async {
|
||||
final server = FakeGeminiServer('gemini-test-a');
|
||||
await server.start();
|
||||
geminiServer = server;
|
||||
geminiAnswers =
|
||||
server.answer((line, socket) => socket.add(utf8.encode('10 Enter a word\r\n')));
|
||||
|
||||
final result =
|
||||
await handler.handle(FetchPage(url: 'gemini://localhost:${server.port}/ask'));
|
||||
|
||||
expect(result, isA<FetchPageOk>());
|
||||
final form = (result as FetchPageOk).page.body.single as FormBlock;
|
||||
final field = form.fields.single as MewsTextField;
|
||||
expect(field.name, 'query');
|
||||
expect(field.password, isFalse);
|
||||
expect(field.label, 'Enter a word');
|
||||
});
|
||||
|
||||
test('a sensitive gemini input asks quietly', () async {
|
||||
final server = FakeGeminiServer('gemini-test-a');
|
||||
await server.start();
|
||||
geminiServer = server;
|
||||
geminiAnswers = server.answer((line, socket) => socket.add(utf8.encode('11 A secret\r\n')));
|
||||
|
||||
final result =
|
||||
await handler.handle(FetchPage(url: 'gemini://localhost:${server.port}/ask'));
|
||||
|
||||
final form = (result as FetchPageOk).page.body.single as FormBlock;
|
||||
expect((form.fields.single as MewsTextField).password, isTrue);
|
||||
});
|
||||
|
||||
test('the first visit to a gemini host reports a pin to save', () async {
|
||||
final server = FakeGeminiServer('gemini-test-a');
|
||||
await server.start();
|
||||
geminiServer = server;
|
||||
geminiAnswers = server
|
||||
.answer((line, socket) => socket.add(utf8.encode('20 text/gemini\r\n$gemtext')));
|
||||
|
||||
final result = await handler.handle(FetchPage(url: 'gemini://localhost:${server.port}/'));
|
||||
|
||||
final ok = result as FetchPageOk;
|
||||
expect(ok.pinUpdates, hasLength(1));
|
||||
expect(ok.pinUpdates.single.host, 'localhost');
|
||||
expect(ok.pinUpdates.single.port, server.port);
|
||||
});
|
||||
|
||||
test('a gemini site asking for a certificate gets the bound identity', () async {
|
||||
final server = FakeGeminiServer('gemini-test-a', clientCertName: 'client-test');
|
||||
await server.start();
|
||||
geminiServer = server;
|
||||
geminiAnswers = server.answer((line, socket) {
|
||||
// The server asks but never checks: an identity is offered or not.
|
||||
if (socket.peerCertificate == null) {
|
||||
socket.add(utf8.encode('60 Who are you\r\n'));
|
||||
} else {
|
||||
socket.add(utf8.encode('20 text/gemini\r\n$gemtext'));
|
||||
}
|
||||
});
|
||||
|
||||
// Without an identity, the request comes back to the reader as a
|
||||
// choice to make.
|
||||
final asked =
|
||||
await handler.handle(FetchPage(url: 'gemini://localhost:${server.port}/'));
|
||||
expect(asked, isA<FetchClientCertificateRequired>());
|
||||
expect((asked as FetchClientCertificateRequired).status, 60);
|
||||
|
||||
// With the identity, the page arrives wearing it.
|
||||
final cert = File('test/fixtures/client-test-cert.pem').readAsStringSync();
|
||||
final key = File('test/fixtures/client-test-key.pem').readAsStringSync();
|
||||
final ok = await handler.handle(FetchPage(
|
||||
url: 'gemini://localhost:${server.port}/', clientCert: cert, clientKey: key));
|
||||
expect(ok, isA<FetchPageOk>());
|
||||
expect((ok as FetchPageOk).page.title, 'Title');
|
||||
});
|
||||
|
||||
test('a capsule image arrives as bytes', () async {
|
||||
final server = FakeSpartanServer();
|
||||
await server.start();
|
||||
spartan = server;
|
||||
final png = base64.decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==');
|
||||
spartanAnswers = server.answer((line, upload, socket) => socket.add([
|
||||
...utf8.encode('2 image/png\r\n'),
|
||||
...png,
|
||||
]));
|
||||
|
||||
final result = await handler
|
||||
.handle(FetchBytes(url: 'spartan://localhost:${server.port}/dot.png'));
|
||||
|
||||
expect(result, isA<FetchBytesOk>());
|
||||
expect((result as FetchBytesOk).data, png);
|
||||
});
|
||||
|
||||
test('a gemini form answers as a query string', () async {
|
||||
final server = FakeGeminiServer('gemini-test-a');
|
||||
await server.start();
|
||||
geminiServer = server;
|
||||
var asked = '';
|
||||
geminiAnswers = server.answer((line, socket) {
|
||||
asked = line;
|
||||
socket.add(utf8.encode('20 text/gemini\r\n$gemtext'));
|
||||
});
|
||||
|
||||
final result = await handler.handle(SubmitForm(
|
||||
url: 'gemini://localhost:${server.port}/ask',
|
||||
method: 'get',
|
||||
fields: {'query': 'phones'}));
|
||||
|
||||
// The fake reads whole lines, so the CRLF is already stripped.
|
||||
expect(asked, 'gemini://localhost:${server.port}/ask?query=phones');
|
||||
expect(result, isA<FetchPageOk>());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue