the-mews/app/test/fake_protocol_server.dart
2026-10-11 22:07:35 +03:00

93 lines
3.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
/// Test doubles speaking the two protocols, so handler- and controller-level
/// tests can exercise the real fetch path against an in-process server.
/// A Gemini server answering every request from [respond], so a test can say
/// exactly what the client must cope with. [respond] gets the request line
/// and writes the raw response, status line and body together.
class FakeGeminiServer {
FakeGeminiServer(this.certName, {this.clientCertName});
final String certName;
/// The certificate this server asks the client for, the way a Gemini site
/// that wants an identity does. Dart's TLS verifies a client certificate
/// against the server's trust, so the test fixture is trusted by name.
final String? clientCertName;
late final int port;
late final SecureServerSocket server;
Future<void> start() async {
final context = SecurityContext()
..useCertificateChain('test/fixtures/$certName-cert.pem')
..usePrivateKey('test/fixtures/$certName-key.pem');
if (clientCertName != null) {
context.setTrustedCertificates('test/fixtures/$clientCertName-cert.pem');
}
server = await SecureServerSocket.bind('localhost', 0, context,
requestClientCertificate: clientCertName != null);
port = server.port;
}
StreamSubscription<SecureSocket> answer(FutureOr<void> Function(String, SecureSocket) respond) {
return server.listen((socket) async {
final line = await socket
.map(utf8.decode)
.transform(const LineSplitter())
.first
.timeout(const Duration(seconds: 5));
await respond(line, socket);
socket.destroy();
});
}
Future<void> close() => server.close();
}
/// A Spartan server answering every request from [respond], which gets the
/// request line and any uploaded bytes, and writes the raw response.
class FakeSpartanServer {
late final int port;
late final ServerSocket server;
Future<void> start() async {
server = await ServerSocket.bind('localhost', 0);
port = server.port;
}
StreamSubscription<Socket> answer(FutureOr<void> Function(String, List<int>, Socket) respond) {
return server.listen((socket) async {
final buffer = BytesBuilder(copy: false);
var lineEnd = -1;
await for (final chunk in socket) {
buffer.add(chunk);
final bytes = buffer.toBytes();
if (lineEnd < 0) lineEnd = _indexOfCrlf(bytes);
if (lineEnd < 0) continue;
final line = utf8.decode(bytes.sublist(0, lineEnd));
final length = int.parse(line.split(' ').last);
// Answer as soon as the line and any upload it announces are here;
// the client keeps the connection open waiting for the response.
if (bytes.length - lineEnd - 2 >= length) {
final upload = bytes.sublist(lineEnd + 2, lineEnd + 2 + length);
await respond(line, upload, socket);
socket.destroy();
return;
}
}
});
}
Future<void> close() => server.close();
}
int _indexOfCrlf(List<int> bytes) {
for (var i = 0; i + 1 < bytes.length; i++) {
if (bytes[i] == 0x0d && bytes[i + 1] == 0x0a) return i;
}
return -1;
}