#include "TestHttpServer.h" #include #include namespace PhpQml::Bridge::Tests { TestHttpServer::TestHttpServer(QObject* parent) : QObject(parent) { m_server.listen(QHostAddress::LocalHost, 0); connect(&m_server, &QTcpServer::newConnection, this, &TestHttpServer::onNewConnection); } QString TestHttpServer::url() const { return QStringLiteral("http://127.0.0.1:%1").arg(m_server.serverPort()); } void TestHttpServer::setResponseBody(const QString& v) { if (m_responseBody == v) return; m_responseBody = v; emit responseBodyChanged(); } void TestHttpServer::setResponseStatus(int v) { if (m_responseStatus == v) return; m_responseStatus = v; emit responseStatusChanged(); } void TestHttpServer::onNewConnection() { while (auto* sock = m_server.nextPendingConnection()) { // One buffer per socket, owned by the socket so it dies with it. // (The original thread_local trick leaked between connections.) auto* buffer = new QByteArray; connect(sock, &QObject::destroyed, [buffer]() { delete buffer; }); connect(sock, &QTcpSocket::readyRead, this, [this, sock, buffer]() { buffer->append(sock->readAll()); const int headerEnd = buffer->indexOf("\r\n\r\n"); if (headerEnd < 0) return; const QByteArray headerBlock = buffer->left(headerEnd); buffer->clear(); const QList lines = headerBlock.split('\n'); QString requestLine; QString authHeader; for (int i = 0; i < lines.size(); ++i) { QByteArray line = lines[i]; if (line.endsWith('\r')) line.chop(1); if (i == 0) { requestLine = QString::fromUtf8(line); continue; } const int colon = line.indexOf(':'); if (colon < 0) continue; const QByteArray name = line.left(colon).trimmed(); const QByteArray value = line.mid(colon + 1).trimmed(); if (name.compare("Authorization", Qt::CaseInsensitive) == 0) { authHeader = QString::fromUtf8(value); } } // Only count + capture metrics for /api/… GETs. SSE reconnect // attempts from MercureClient hit /.well-known/mercure on the // same port and would otherwise inflate the request count and // overwrite the captured headers we want to assert against. const bool isApiGet = requestLine.startsWith(QStringLiteral("GET /api/")); if (isApiGet) { if (m_lastRequestLine != requestLine) { m_lastRequestLine = requestLine; emit lastRequestLineChanged(); } if (m_lastAuthHeader != authHeader) { m_lastAuthHeader = authHeader; emit lastAuthHeaderChanged(); } ++m_apiGetCount; emit apiGetCountChanged(); } // For /api/ routes, mimic SessionAuthenticator and reject // requests without an Authorization header. This is what // exposes the property-order race in the regression test: // pre-fix, the GET went out unauthenticated, this server // returned 401, and the model parked with `ready === false`. const bool needAuth = isApiGet; const bool isAuthed = !authHeader.isEmpty(); const bool reject = needAuth && !isAuthed; const int status = reject ? 401 : m_responseStatus; const QByteArray body = reject ? QByteArrayLiteral(R"({"type":"about:blank","title":"Unauthorized","status":401})") : m_responseBody.toUtf8(); QByteArray resp; resp.append("HTTP/1.1 ").append(QByteArray::number(status)) .append(' ').append(status == 200 ? "OK" : "STATUS").append("\r\n"); resp.append("Content-Type: application/json\r\n"); resp.append("Content-Length: ").append(QByteArray::number(body.size())).append("\r\n"); resp.append("Connection: close\r\n\r\n"); resp.append(body); sock->write(resp); sock->disconnectFromHost(); }); connect(sock, &QTcpSocket::disconnected, sock, &QObject::deleteLater); } } } // namespace PhpQml::Bridge::Tests