feat: add Gemini and Spartan protocol support

This commit is contained in:
randogoth 2026-10-11 22:07:25 +03:00
parent a4a49f41d7
commit fa7aa5557c
48 changed files with 4992 additions and 161 deletions

129
app/test/spartan_test.dart Normal file
View file

@ -0,0 +1,129 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'fake_protocol_server.dart';
import 'package:mews/protocols/spartan.dart';
Uri open(String path, FakeSpartanServer server) =>
Uri.parse('spartan://localhost:${server.port}$path');
void main() {
late FakeSpartanServer server;
late StreamSubscription<Socket> answers;
final client = SpartanClient(timeout: const Duration(seconds: 5));
tearDown(() async {
await answers.cancel();
await server.close();
});
test('a success answers with status, meta and body', () async {
server = FakeSpartanServer();
await server.start();
var requested = '';
answers = server.answer((line, upload, socket) {
requested = line;
socket.add(utf8.encode('2 text/gemini\r\nhello'));
});
final response = await client.fetch(open('/page', server));
expect(response.status, 2);
expect(response.meta, 'text/gemini');
expect(utf8.decode(response.body!), 'hello');
expect(requested, 'localhost /page 0');
});
test('an upload rides behind the request line', () async {
server = FakeSpartanServer();
await server.start();
var requested = '';
var uploaded = '';
answers = server.answer((line, upload, socket) {
requested = line;
uploaded = utf8.decode(upload);
socket.add(utf8.encode('2 text/gemini\r\nok'));
});
final response =
await client.fetch(open('/echo', server), upload: utf8.encode('payload'));
expect(response.status, 2);
expect(requested, 'localhost /echo 7');
expect(uploaded, 'payload');
});
test('a query is part of the path', () async {
server = FakeSpartanServer();
await server.start();
var requested = '';
answers = server.answer((line, upload, socket) {
requested = line;
socket.add(utf8.encode('2 text/gemini\r\nok'));
});
await client.fetch(open('/search?q=phones', server));
expect(requested, 'localhost /search?q=phones 0');
});
test('a redirect is followed to the final address', () async {
server = FakeSpartanServer();
await server.start();
final requests = <String>[];
answers = server.answer((line, upload, socket) async {
requests.add(line);
if (line.contains('/here')) {
socket.add(utf8.encode('3 ${open('/there', server)}\r\n'));
} else {
socket.add(utf8.encode('2 text/gemini\r\narrived'));
}
});
final response = await client.fetch(open('/here', server));
expect(response.status, 2);
expect(utf8.decode(response.body!), 'arrived');
expect(response.url.path, '/there');
expect(requests, hasLength(2));
});
test('a redirect loop gives up after five hops', () async {
server = FakeSpartanServer();
await server.start();
final requests = <String>[];
answers = server.answer((line, upload, socket) async {
requests.add(line);
socket.add(utf8.encode('3 ${open('/loop', server)}\r\n'));
});
await expectLater(
client.fetch(open('/loop', server)),
throwsA(isA<SpartanTooManyRedirects>()),
);
expect(requests, hasLength(6));
});
test('an error status carries its message', () async {
server = FakeSpartanServer();
await server.start();
answers = server.answer((line, upload, socket) =>
socket.add(utf8.encode('4 Nothing lives here\r\n')));
final response = await client.fetch(open('/gone', server));
expect(response.status, 4);
expect(response.meta, 'Nothing lives here');
expect(response.body, isNull);
});
test('an unreachable host is one failure', () async {
final nowhere = Uri.parse('spartan://localhost:1/');
await expectLater(client.fetch(nowhere), throwsA(isA<SpartanException>()));
});
}