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 = ''' Parsed

Some content.

'''; 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()); 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: '

nope')); expect(result, isA()); expect((result as FetchParseError).source, '

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? spartanAnswers; StreamSubscription? 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()); final ok = result as FetchPageOk; expect(ok.page.title, 'Title'); expect(ok.page.isMews, isTrue); expect(ok.page.body.first, isA()); 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()); 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()); 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()); 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()); 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()); 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()); }); }); }