127 lines
7.5 KiB
Java
127 lines
7.5 KiB
Java
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<String> 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<String, Long> 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<String, String> 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());
|
|
}
|
|
}
|