the-mews/app/test/fetch_actor_test.dart

211 lines
7.6 KiB
Dart
Raw Normal View History

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"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head><title>Parsed</title><meta name="mews-profile" content="0.1" /></head>
<body><p>Some content.</p></body>
</html>
''';
const gemtext = '# Title\n\nSome text.\n';
void main() {
test('ParseSource parses without touching the network', () async {
final actor = Actor.create(FetchHandler.new);
addTearDown(actor.close);
final result = await actor
.send(const ParseSource(url: 'https://example.com/parsed.html', source: page));
expect(result, isA<FetchPageOk>());
final ok = result as FetchPageOk;
expect(ok.page.title, 'Parsed');
expect(ok.page.isMews, isTrue);
expect(ok.notModified, isFalse);
});
test('ParseSource reports broken markup with its source', () async {
final actor = Actor.create(FetchHandler.new);
addTearDown(actor.close);
final result = await actor.send(
const ParseSource(url: 'https://example.com/broken.html', source: '<p>nope'));
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>());
});
});
}