import com.sun.net.httpserver.*; import java.io.*; import java.net.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.security.*; import java.util.*; import java.util.concurrent.*; /** Synthetic demo provider. No database, provider calls or request-body logging. */ public final class MockServer { static final Set NAMES = Set.of("clientes_consulta", "buro_consulta", "documento_valida", "documents_metadata"); static final int MAX_BYTES = 262144; final Path data; final String token; final Map calls = new HashMap<>(); record Reply(int status, int delay, String body) {} MockServer(Path data, String token) throws IOException { this.data = data; this.token = token; Files.createDirectories(data); for (String name : NAMES) { calls.put(name, 0L); if (!Files.exists(data.resolve(name + ".properties"))) { String body = switch (name) { case "clientes_consulta" -> "{\"found\":true,\"customerId\":\"synthetic-customer\"}"; case "buro_consulta" -> "{\"score\":71,\"consultationId\":\"synthetic-consultation\"}"; case "documento_valida" -> "{\"status\":\"VALID\",\"reasonCodes\":[]}"; default -> "{\"error\":\"Configure metadata for this request and document before use\"}"; }; save(name, new Reply(name.equals("documents_metadata") ? 404 : 200, 0, body)); } } } synchronized Reply read(String name) throws IOException { Properties p = new Properties(); try (Reader r = Files.newBufferedReader(data.resolve(name + ".properties"))) { p.load(r); } return checked(p.getProperty("status"), p.getProperty("delayMs"), p.getProperty("body", "")); } static Reply checked(String status, String delay, String body) { int s = Integer.parseInt(status), d = Integer.parseInt(delay); if (s < 200 || s > 599 || s == 204 || s == 205 || s == 304 || d < 0 || d > 60000 || body.getBytes(StandardCharsets.UTF_8).length > MAX_BYTES) throw new IllegalArgumentException(); return new Reply(s, d, body); } synchronized void save(String name, Reply reply) throws IOException { Properties p = new Properties(); p.setProperty("status", Integer.toString(reply.status())); p.setProperty("delayMs", Integer.toString(reply.delay())); p.setProperty("body", reply.body()); Path tmp = Files.createTempFile(data, name, ".tmp"); try { try (Writer w = Files.newBufferedWriter(tmp)) { p.store(w, "Demo response; updated via admin API"); } Files.move(tmp, data.resolve(name + ".properties"), StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } finally { Files.deleteIfExists(tmp); } } synchronized long count(String name, boolean increment) { if (increment) calls.put(name, calls.get(name) + 1); return calls.get(name); } static void send(HttpExchange x, int status, String body) throws IOException { byte[] bytes = body.getBytes(StandardCharsets.UTF_8); x.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); x.getResponseHeaders().set("Cache-Control", "no-store"); x.sendResponseHeaders(status, bytes.length); x.getResponseBody().write(bytes); } void handle(HttpExchange x) throws IOException { try { String path = x.getRequestURI().getPath(), method = x.getRequestMethod(); if (path.equals("/health") && method.equals("GET")) { send(x, 200, "{\"status\":\"UP\"}"); return; } boolean admin = path.startsWith("/__admin/"); if (admin && !MessageDigest.isEqual(("Bearer " + token).getBytes(StandardCharsets.UTF_8), Objects.toString(x.getRequestHeaders().getFirst("Authorization"), "").getBytes(StandardCharsets.UTF_8))) { send(x, 401, "{\"error\":\"Unauthorized\"}"); return; } String name = path.substring(admin ? "/__admin/".length() : 1); if (!NAMES.contains(name)) { send(x, 404, "{\"error\":\"Unknown provider\"}"); return; } if (admin && method.equals("PUT")) { byte[] bytes = x.getRequestBody().readNBytes(MAX_BYTES + 1); if (bytes.length > MAX_BYTES) { send(x, 413, "{\"error\":\"Body too large\"}"); return; } Reply reply = checked(Objects.toString(x.getRequestHeaders().getFirst("X-Mock-Status"), "200"), Objects.toString(x.getRequestHeaders().getFirst("X-Mock-Delay-Ms"), "0"), new String(bytes, StandardCharsets.UTF_8)); save(name, reply); send(x, 200, "{\"saved\":true}"); return; } if ((admin && !method.equals("GET")) || (!admin && !method.equals("POST"))) { send(x, 405, "{\"error\":\"Method not allowed\"}"); return; } if (!admin && x.getRequestBody().readNBytes(MAX_BYTES + 1).length > MAX_BYTES) { send(x, 413, "{\"error\":\"Body too large\"}"); return; } Reply reply = read(name); x.getResponseHeaders().set("X-Mock-Status", Integer.toString(reply.status())); x.getResponseHeaders().set("X-Mock-Delay-Ms", Integer.toString(reply.delay())); x.getResponseHeaders().set("X-Mock-Calls", Long.toString(count(name, !admin))); if (!admin) Thread.sleep(reply.delay()); send(x, admin ? 200 : reply.status(), reply.body()); } catch (IllegalArgumentException e) { send(x, 400, "{\"error\":\"Invalid status or delay; status 200..599 excluding 204/205/304, delay 0..60000\"}"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (IOException e) { // A client timeout/disconnect is expected when simulating a slow provider. if (x.getResponseCode() == -1) send(x, 500, "{\"error\":\"Response storage unavailable\"}"); } finally { x.close(); } } public static void main(String[] args) throws Exception { if (args.length == 1 && args[0].equals("--health")) { var connection = (HttpURLConnection) URI.create("http://localhost:" + System.getenv().getOrDefault("MOCK_PORT", "8788") + "/health").toURL().openConnection(); connection.setConnectTimeout(2000); connection.setReadTimeout(2000); try { if (connection.getResponseCode() != 200) System.exit(1); } finally { connection.disconnect(); } return; } Map env = System.getenv(); String token = env.getOrDefault("MOCK_ADMIN_TOKEN", ""); if (token.isBlank()) throw new IllegalArgumentException("MOCK_ADMIN_TOKEN is required"); String host = env.getOrDefault("MOCK_BIND", "127.0.0.1"); int port = Integer.parseInt(env.getOrDefault("MOCK_PORT", "8788")); HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 32); MockServer handler = new MockServer(Path.of(env.getOrDefault("MOCK_DATA", "data")), token); ExecutorService executor = Executors.newFixedThreadPool(16); server.setExecutor(executor); server.createContext("/", handler::handle); Runtime.getRuntime().addShutdownHook(new Thread(() -> { server.stop(1); executor.shutdownNow(); })); server.start(); System.out.println("Demo providers listening on " + host + ":" + server.getAddress().getPort()); } }