Files
qbit-web-performance/src/test/java/com/kingsrook/qbits/webperf/FixtureServer.java
T
james.maesandClaude Opus 5.5 11b652ae0c feat: P1 lab and field data (PSI/CrUX runners, queue worker, actions, widgets)
- PsiLabRunner (PageSpeed Insights v5, every category, key never in errors)
  and LabResultParser (Lighthouse 13 scores, metrics, LCP element, findings
  keyed by audit id and Lighthouse version, PSI field data)
- CruxClient (CrUX queryRecord; 404 is NO_DATA, stored as such)
- WebPerfService: sitemap sync (nothing deleted), queue with merge and
  daily cap (SKIPPED_QUOTA), worker with conditional claim, retry with
  backoff, findings, gzipped full report, latest page results; CrUX upsert
- standard streamed ETL actions: Test page speed (pages and content
  records), Run site audit, Sync pages from sitemap, Retry, Refresh Google
  field data; hidden scheduled worker, daily key pages, weekly audit,
  daily sync and CrUX
- Performance home widgets (Site Scores, Trend, Slowest Pages, Top
  Opportunities, Google Field Data, Recent Runs) and the record
  Performance panel; readable at every width, real data only
- tests on H2 with the real changelog and a real Lighthouse 13 result of
  www.kof22.com, Google stood in by a local HTTP server

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:37:26 -05:00

161 lines
5.3 KiB
Java

/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.webperf;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import com.sun.net.httpserver.HttpServer;
/*******************************************************************************
** A local HTTP server standing in for the Google APIs in tests: it answers
** with queued responses (the last one repeats), and records every request's
** query string and body.
*******************************************************************************/
public class FixtureServer implements AutoCloseable
{
/*******************************************************************************
** One response.
*******************************************************************************/
public record Response(int status, String body)
{
}
/*******************************************************************************
** One recorded request.
*******************************************************************************/
public record Request(String method, String query, String body)
{
}
private final HttpServer server;
private final Deque<Response> responses = new ArrayDeque<>();
private final List<Request> requests = new CopyOnWriteArrayList<>();
/*******************************************************************************
** Start a server.
*******************************************************************************/
public FixtureServer() throws IOException
{
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/", exchange ->
{
String body;
try(InputStream in = exchange.getRequestBody())
{
body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
requests.add(new Request(exchange.getRequestMethod(), exchange.getRequestURI().getRawQuery(), body));
Response response;
synchronized(responses)
{
response = responses.size() > 1 ? responses.pollFirst() : responses.peekFirst();
}
if(response == null)
{
response = new Response(500, "{\"error\":{\"message\":\"no response configured\"}}");
}
byte[] bytes = response.body().getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "application/json");
exchange.sendResponseHeaders(response.status(), bytes.length);
try(OutputStream out = exchange.getResponseBody())
{
out.write(bytes);
}
exchange.close();
});
server.start();
}
/*******************************************************************************
** Queue responses (the last one repeats).
*******************************************************************************/
public FixtureServer respond(Response... queued)
{
synchronized(responses)
{
responses.clear();
responses.addAll(List.of(queued));
}
return (this);
}
/*******************************************************************************
** A fixture file's content.
*******************************************************************************/
public static String fixture(String name) throws IOException
{
try(InputStream in = FixtureServer.class.getClassLoader().getResourceAsStream("fixtures/" + name))
{
return (new String(in.readAllBytes(), StandardCharsets.UTF_8));
}
}
/*******************************************************************************
** The server's base URL.
*******************************************************************************/
public String url(String path)
{
return ("http://127.0.0.1:" + server.getAddress().getPort() + path);
}
/*******************************************************************************
** Requests so far.
*******************************************************************************/
public List<Request> getRequests()
{
return (requests);
}
/*******************************************************************************
** Stop.
*******************************************************************************/
@Override
public void close()
{
server.stop(0);
}
}