feat: implement MoloApp plans 01-15 with integration
Full C++23 framework tree: runtime, observability, security, data, app, realtime, supervisor, clients, CLI, example plus enterprise tenancy, audit, operability, governance, supplychain. Postgres via docker compose for tests.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
// Route: POST /auth/login
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto ok = f.client->post("/auth/login",
|
||||
"{\"username\":\"ada\",\"password\":"
|
||||
"\"correct-horse\"}",
|
||||
"application/json");
|
||||
check(ok.status == 200, "good credentials are 200");
|
||||
check(contains(ok.body, "\"user\":\"ada\""), "username echoed");
|
||||
auto bad = f.client->post("/auth/login",
|
||||
"{\"username\":\"ada\",\"password\":\"wrong\"}",
|
||||
"application/json");
|
||||
check(bad.status == 403, "bad password is 403");
|
||||
auto missing = f.client->post("/auth/login", "{\"username\":\"ada\"}",
|
||||
"application/json");
|
||||
check(missing.status == 400, "missing field is 400");
|
||||
return finish("auth/login");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Route: GET /auth/me (Basic auth)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
using molo::example::fw::Method;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto anon = f.client->get("/auth/me");
|
||||
check(anon.status == 401, "no header is 401");
|
||||
auto bad = f.client->request(
|
||||
Method::Get, "/auth/me", "",
|
||||
{{"Authorization", "Basic YWRhOndyb25n"}});
|
||||
check(bad.status == 403, "bad password is 403");
|
||||
auto ok = f.client->request(
|
||||
Method::Get, "/auth/me", "",
|
||||
{{"Authorization", "Basic YWRhOmNvcnJlY3QtaG9yc2U="}});
|
||||
check(ok.status == 200, "good credentials are 200");
|
||||
check(contains(ok.body, "\"user\":\"ada\""), "identity returned");
|
||||
return finish("auth/me");
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
// Route: /cache/lookup (miss then hit)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto miss = f.client->get("/cache/lookup?key=k1");
|
||||
check(contains(miss.body, "\"hit\":false"), "first lookup misses");
|
||||
auto hit = f.client->get("/cache/lookup?key=k1");
|
||||
check(contains(hit.body, "\"hit\":true"), "second lookup hits");
|
||||
check(contains(hit.body, "computed-for-k1"), "same value served");
|
||||
return finish("cache/lookup");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: /app1/app1/app1/whoami (application chaining, depth 3)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/app1/app1/app1/whoami");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"depth\":3"), "three mounts merged");
|
||||
auto shallow = f.client->get("/app1/whoami");
|
||||
check(shallow.status == 404, "partial chain is 404");
|
||||
return finish("chain/deep");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Route: GET /cli/ai (AI harness sample, secrets redacted)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/cli/ai?prompt=add+a+route");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "devplace.net/openai/v1"), "devplace base url");
|
||||
check(contains(res.body, "\"model\":\"molodetz\""), "molodetz model");
|
||||
auto secret = f.client->get("/cli/ai?prompt=DEVPLACE_API_KEY%3Dabc123");
|
||||
check(!contains(secret.body, "abc123"), "secret redacted");
|
||||
check(contains(secret.body, "REDACTED"), "redaction marker present");
|
||||
return finish("cli/ai");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Route: GET /embedded/{name} (embedded C++ assets)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/embedded/hello.txt");
|
||||
check(res.status == 200, "status 200");
|
||||
check(res.headers.get("X-Asset-Source") == "embedded",
|
||||
"served from embedded bundle");
|
||||
check(contains(res.body, "embedded assets"), "bundle content served");
|
||||
auto missing = f.client->get("/embedded/nope.txt");
|
||||
check(missing.status == 404, "missing asset is 404");
|
||||
return finish("embedded/assets");
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Route: /events/fire (typed event bus)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
int seen = 0;
|
||||
f.services.events->on("demo.ping",
|
||||
[&](const molo::example::fw::Event&) { ++seen; });
|
||||
auto fire = f.client->post(
|
||||
"/events/fire", "{\"type\":\"demo.ping\",\"payload\":\"p1\"}",
|
||||
"application/json");
|
||||
check(fire.status == 200, "fire status 200");
|
||||
check(seen == 1, "side effect: listener ran");
|
||||
auto list = f.client->get("/events/fire");
|
||||
check(contains(list.body, "demo.ping"), "delivery recorded");
|
||||
return finish("events/fire");
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Route: GET /formats/data (content negotiation)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
using molo::example::fw::Method;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto def = f.client->get("/formats/data");
|
||||
check(contains(def.headers.get("Content-Type"), "application/json"),
|
||||
"default is json");
|
||||
auto xml = f.client->request(Method::Get, "/formats/data", "",
|
||||
{{"Accept", "application/xml"}});
|
||||
check(contains(xml.headers.get("Content-Type"), "application/xml"),
|
||||
"xml negotiated");
|
||||
check(contains(xml.body, "<value>42</value>"), "xml body");
|
||||
auto weighted = f.client->request(
|
||||
Method::Get, "/formats/data", "",
|
||||
{{"Accept", "text/html;q=0.2, text/plain;q=0.9"}});
|
||||
check(contains(weighted.headers.get("Content-Type"), "text/plain"),
|
||||
"q weights honored");
|
||||
auto nope = f.client->request(Method::Get, "/formats/data", "",
|
||||
{{"Accept", "image/png"}});
|
||||
check(nope.status == 406, "no match is 406");
|
||||
return finish("formats/data");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Route: /forms/contact (form render plus validation)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto form = f.client->get("/forms/contact");
|
||||
check(form.status == 200, "form renders 200");
|
||||
check(contains(form.body, "<form"), "form markup present");
|
||||
auto ok = f.client->post("/forms/contact",
|
||||
"name=ada&email=ada@x.io&message=hi",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(ok.status == 200, "valid post is 200");
|
||||
check(contains(ok.body, "Thanks ada"), "success page names user");
|
||||
auto bad = f.client->post("/forms/contact", "name=ada",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(bad.status == 422, "invalid post is 422");
|
||||
check(contains(bad.body, "validation_error"), "failures listed");
|
||||
return finish("forms/contact");
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Route: GET /health (probe plus real-HTTP loopback round-trip)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
#include "fw/Server.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/health");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"status\":\"ok\""), "ok body");
|
||||
|
||||
molo::example::fw::Server server(f.app);
|
||||
check(server.start("127.0.0.1", 0), "loopback server starts");
|
||||
std::string wire = molo::example::fw::http_roundtrip(
|
||||
"127.0.0.1", server.port(),
|
||||
"GET /health HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");
|
||||
check(contains(wire, "200 OK"), "real HTTP status line");
|
||||
check(contains(wire, "\"status\":\"ok\""), "real HTTP body");
|
||||
server.stop();
|
||||
|
||||
auto missing = f.client->get("/no-such-route");
|
||||
check(missing.status == 404, "unknown route is 404");
|
||||
return finish("health");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Route: GET / (index, template inheritance, coverage matrix)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.headers.get("Content-Type"), "text/html"),
|
||||
"html content type");
|
||||
check(contains(res.body, "Living example"), "matrix heading");
|
||||
check(contains(res.body, "/routing/hello/world"), "matrix links a route");
|
||||
check(contains(res.body, "viewport"), "responsive viewport meta");
|
||||
check(contains(res.body, "nav-narrow"), "narrow nav dropdown present");
|
||||
check(res.headers.has("X-Request-ID"), "request id middleware ran");
|
||||
return finish("index");
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Route: /kv/items
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto missing = f.client->get("/kv/items?key=nope");
|
||||
check(missing.status == 404, "missing key is 404");
|
||||
auto bad = f.client->post("/kv/items", "{\"key\":\"a\"}",
|
||||
"application/json");
|
||||
check(bad.status == 400, "key without value is 400");
|
||||
auto put = f.client->post("/kv/items", "{\"key\":\"a\",\"value\":\"b\"}",
|
||||
"application/json");
|
||||
check(put.status == 201, "store status 201");
|
||||
auto get = f.client->get("/kv/items?key=a");
|
||||
check(contains(get.body, "\"value\":\"b\""), "round trip works");
|
||||
return finish("kv/items");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Route: GET /metrics/counters (prometheus render)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto first = f.client->get("/metrics/counters");
|
||||
check(first.status == 200, "status 200");
|
||||
check(contains(first.body, "example.page.views 1"), "counter at 1");
|
||||
f.client->get("/metrics/counters");
|
||||
check(f.services.metrics->get("example.page.views") == 2.0,
|
||||
"side effect: counter at 2");
|
||||
return finish("metrics/counters");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Routes: GET /openapi.json and GET /docs
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto spec = f.client->get("/openapi.json");
|
||||
check(spec.status == 200, "spec status 200");
|
||||
check(contains(spec.body, "\"openapi\":\"3.0.0\""), "openapi version");
|
||||
check(contains(spec.body, "/health"), "health path listed");
|
||||
auto docs = f.client->get("/docs");
|
||||
check(docs.status == 200, "docs status 200");
|
||||
check(contains(docs.body, "/openapi.json"), "docs link spec");
|
||||
return finish("openapi/docs");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Route: /orm/users (dual backends, auto-sync self-healing)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto first = f.client->get("/orm/users?backend=sqlite");
|
||||
check(first.status == 200, "sqlite status 200");
|
||||
check(contains(first.body, "\"healed\":true"), "table auto-created");
|
||||
auto insert = f.client->post("/orm/users?backend=sqlite",
|
||||
"{\"name\":\"ada\"}", "application/json");
|
||||
check(insert.status == 201, "insert status 201");
|
||||
auto widen = f.client->post("/orm/users?backend=sqlite",
|
||||
"{\"name\":\"g\",\"email\":\"g@x.io\"}",
|
||||
"application/json");
|
||||
check(contains(widen.body, "auto-added column email"),
|
||||
"new column self-healed");
|
||||
auto pg = f.client->get("/orm/users?backend=postgres");
|
||||
check(contains(pg.body, "\"backend\":\"postgres\""),
|
||||
"postgres backend selected");
|
||||
check(!f.services.sqlite->tables().empty(), "side effect: table exists");
|
||||
return finish("orm/users");
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Route: /pubsub/publish
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
int seen = 0;
|
||||
f.services.pubsub->subscribe("news",
|
||||
[&](const std::string&) { ++seen; });
|
||||
auto pub = f.client->post("/pubsub/publish",
|
||||
"{\"topic\":\"news\",\"message\":\"hi\"}",
|
||||
"application/json");
|
||||
check(pub.status == 200, "publish status 200");
|
||||
check(contains(pub.body, "\"delivered\":1"), "one subscriber reached");
|
||||
check(seen == 1, "side effect: listener ran");
|
||||
auto hist = f.client->get("/pubsub/publish?topic=news");
|
||||
check(contains(hist.body, "hi"), "history kept");
|
||||
return finish("pubsub/publish");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: GET /redirect/here (temporary vs permanent)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto temp = f.client->get("/redirect/here");
|
||||
check(temp.status == 302, "default is temporary");
|
||||
check(temp.headers.get("Location") == "/health", "location set");
|
||||
auto perm = f.client->get("/redirect/here?mode=perm");
|
||||
check(perm.status == 301, "perm mode is permanent");
|
||||
return finish("redirect/here");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Route: /replication/state (append plus since)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto add = f.client->post("/replication/state",
|
||||
"{\"object\":\"doc-1\",\"data\":\"v1\"}",
|
||||
"application/json");
|
||||
check(add.status == 201, "append is 201");
|
||||
check(contains(add.body, "last-writer-wins"), "conflict rule named");
|
||||
auto full = f.client->get("/replication/state?since=0");
|
||||
check(contains(full.body, "doc-1"), "entry replicated");
|
||||
auto empty = f.client->get("/replication/state?since=99");
|
||||
check(contains(empty.body, "\"entries\":[]"), "cursor filters");
|
||||
return finish("replication/state");
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Route: GET /restclient/self (REST client self-call)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/restclient/self");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"self_call\":200"), "self call reached 200");
|
||||
return finish("restclient/self");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Route: GET /routing/hello/{name}
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/routing/hello/ada");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"hello\":\"ada\""), "path param bound");
|
||||
auto encoded = f.client->get("/routing/hello/a%20b");
|
||||
check(contains(encoded.body, "\"hello\":\"a b\""), "param url-decoded");
|
||||
auto missing = f.client->get("/routing/hello");
|
||||
check(missing.status == 404, "missing segment is 404");
|
||||
return finish("routing/hello");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: /routing/routers/nested/deep (nested router include)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/routing/routers/nested/deep");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"nested\":true"), "nested router reached");
|
||||
auto shallow = f.client->get("/routing/routers");
|
||||
check(shallow.status == 404, "prefix alone is 404");
|
||||
return finish("routing/routers");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Route: /routing/views/greet (class-based view, GET plus POST)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto get = f.client->get("/routing/views/greet?name=grace");
|
||||
check(get.status == 200, "GET status 200");
|
||||
check(contains(get.body, "GreetView"), "view dispatched GET");
|
||||
check(contains(get.body, "grace"), "query value used");
|
||||
auto post = f.client->post("/routing/views/greet", "name=ada",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(post.status == 200, "POST status 200");
|
||||
check(contains(post.body, "\"method\":\"POST\""), "view dispatched POST");
|
||||
auto put = f.client->request(molo::example::fw::Method::Put,
|
||||
"/routing/views/greet");
|
||||
check(put.status == 404 || put.status == 405,
|
||||
"unregistered method rejected");
|
||||
return finish("routing/views");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Route: GET /sessions/counter (signed cookie session)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto first = f.client->get("/sessions/counter");
|
||||
check(first.status == 200, "status 200");
|
||||
check(contains(first.body, "\"count\":1"), "counter starts at 1");
|
||||
check(contains(first.body, "\"fresh\":true"), "first visit is fresh");
|
||||
check(first.headers.has("Set-Cookie"), "session cookie set");
|
||||
auto second = f.client->get("/sessions/counter");
|
||||
check(contains(second.body, "\"count\":2"), "jar keeps session");
|
||||
check(contains(second.body, "\"fresh\":false"), "second visit resumes");
|
||||
return finish("sessions/counter");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Route: /socketio/rooms (socket.io skeleton endpoint)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto info = f.client->get("/socketio/rooms");
|
||||
check(info.status == 200, "status 200");
|
||||
check(contains(info.body, "socket.io"), "protocol named");
|
||||
check(contains(info.body, "long-polling"), "fallback named");
|
||||
auto emit = f.client->post(
|
||||
"/socketio/rooms", "{\"room\":\"lobby\",\"event\":\"join\"}",
|
||||
"application/json");
|
||||
check(contains(emit.body, "\"ack\":true"), "emit acknowledged");
|
||||
return finish("socketio/rooms");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: GET /sse/events
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/sse/events");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.headers.get("Content-Type"), "text/event-stream"),
|
||||
"sse content type");
|
||||
check(contains(res.body, "data: hello"), "event framed");
|
||||
return finish("sse/events");
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Route: GET /static/{name} (disk static, traversal guard)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto css = f.client->get("/static/app.css");
|
||||
check(css.status == 200, "css status 200");
|
||||
check(contains(css.headers.get("Content-Type"), "text/css"),
|
||||
"css content type");
|
||||
check(css.headers.has("ETag"), "etag present");
|
||||
check(contains(css.body, "nav-narrow"), "stylesheet served");
|
||||
auto missing = f.client->get("/static/nope.css");
|
||||
check(missing.status == 404, "missing file is 404");
|
||||
return finish("static/files");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Route: GET /streaming/feed
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/streaming/feed");
|
||||
check(res.status == 200, "status 200");
|
||||
check(res.headers.get("Transfer-Encoding") == "chunked",
|
||||
"chunked encoding marked");
|
||||
check(contains(res.body, "chunk-1"), "first chunk present");
|
||||
check(contains(res.body, "chunk-3"), "last chunk present");
|
||||
return finish("streaming/feed");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: GET /streaming/passthrough
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/streaming/passthrough?target=/health");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "passthrough from /health"),
|
||||
"upstream named");
|
||||
check(contains(res.body, "\"status\":\"ok\""), "upstream body relayed");
|
||||
return finish("streaming/passthrough");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Route: /supervisor/tasks (enqueue plus list)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto add = f.client->post("/supervisor/tasks",
|
||||
"{\"kind\":\"email\",\"payload\":\"{}\" }",
|
||||
"application/json");
|
||||
check(add.status == 201, "enqueue is 201");
|
||||
check(contains(add.body, "\"id\":\"task-"), "task id issued");
|
||||
auto list = f.client->get("/supervisor/tasks");
|
||||
check(contains(list.body, "task-1"), "task listed");
|
||||
check(f.services.tasks->list().size() == 1,
|
||||
"side effect: queue holds one");
|
||||
return finish("supervisor/tasks");
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Route: GET /supervisor/workers
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/supervisor/workers");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"name\":\"mailer\""), "mailer listed");
|
||||
check(contains(res.body, "\"name\":\"indexer\""), "indexer listed");
|
||||
check(contains(res.body, "\"running\":true"), "seed worker runs");
|
||||
return finish("supervisor/workers");
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
// Shared test support for the living example suite. Not a test itself: every
|
||||
// test file owns one main() and uses these helpers for loud assertions.
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include "app/App.hpp"
|
||||
#include "fw/TestClient.hpp"
|
||||
|
||||
namespace molo::example::test {
|
||||
|
||||
inline int g_failures = 0;
|
||||
|
||||
inline void check(bool cond, const std::string& what) {
|
||||
if (cond) {
|
||||
std::cout << " ok: " << what << "\n";
|
||||
} else {
|
||||
++g_failures;
|
||||
std::cout << " FAIL: " << what << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
inline bool contains(const std::string& hay, const std::string& needle) {
|
||||
return hay.find(needle) != std::string::npos;
|
||||
}
|
||||
|
||||
inline int finish(const std::string& name) {
|
||||
if (g_failures == 0) {
|
||||
std::cout << "PASS: " << name << "\n";
|
||||
return 0;
|
||||
}
|
||||
std::cout << "FAIL: " << name << " (" << g_failures << " failures)\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
molo::example::app::Services services;
|
||||
std::shared_ptr<molo::example::fw::Application> app;
|
||||
std::shared_ptr<molo::example::fw::TestClient> client;
|
||||
|
||||
Fixture() {
|
||||
app = molo::example::app::build_application(services);
|
||||
client = std::make_shared<molo::example::fw::TestClient>(app);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace molo::example::test
|
||||
@@ -0,0 +1,16 @@
|
||||
// Route: GET /telemetry/trace (span plus correlation id)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/telemetry/trace");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "\"trace\":\"trace-"), "trace id issued");
|
||||
check(contains(res.body, "\"correlation\":\"req-"),
|
||||
"correlation id flows");
|
||||
check(f.services.telemetry->spans().size() == 1,
|
||||
"side effect: span recorded");
|
||||
return finish("telemetry/trace");
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Route: GET /templates/page (extends, blocks, for, filters, escape)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto res = f.client->get("/templates/page");
|
||||
check(res.status == 200, "status 200");
|
||||
check(contains(res.body, "<title>Templates</title>"),
|
||||
"base layout applied");
|
||||
check(contains(res.body, "Author: ADA"), "upper filter ran");
|
||||
check(contains(res.body, "<b>bold</b>"),
|
||||
"default output escaped");
|
||||
check(contains(res.body, "<b>bold</b>"), "safe opt-out kept markup");
|
||||
check(contains(res.body, "extends (inheritance)"), "for loop rendered");
|
||||
return finish("templates/page");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Route: POST /uploads/file
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
using molo::example::fw::Method;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto ok = f.client->post("/uploads/file?filename=note.txt", "hello",
|
||||
"text/plain");
|
||||
check(ok.status == 201, "allowed upload is 201");
|
||||
check(contains(ok.body, "\"size\":5"), "size reported");
|
||||
auto evil = f.client->request(Method::Post,
|
||||
"/uploads/file?filename=x.bin", "data",
|
||||
{{"Content-Type", "application/x-evil"}});
|
||||
check(evil.status == 415, "blocked content type is 415");
|
||||
auto traversal = f.client->post("/uploads/file?filename=../x.txt", "d",
|
||||
"text/plain");
|
||||
check(traversal.status == 400, "unsafe filename is 400");
|
||||
check(f.services.uploads->list().size() == 1,
|
||||
"side effect: one upload stored");
|
||||
return finish("uploads/file");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Route: POST /validate/user (all-failures validation error)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto ok = f.client->post(
|
||||
"/validate/user",
|
||||
"{\"username\":\"ada\",\"email\":\"ada@x.io\",\"age\":\"36\"}",
|
||||
"application/json");
|
||||
check(ok.status == 200, "valid model is 200");
|
||||
auto bad = f.client->post("/validate/user",
|
||||
"{\"username\":\"ad\",\"age\":\"old\"}",
|
||||
"application/json");
|
||||
check(bad.status == 422, "invalid model is 422");
|
||||
check(contains(bad.body, "username"), "username failure listed");
|
||||
check(contains(bad.body, "email"), "missing email listed");
|
||||
check(contains(bad.body, "age"), "bad age listed");
|
||||
return finish("validate/user");
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Route: /webdav/files/ (PROPFIND, GET, PUT, DELETE)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
using molo::example::fw::Method;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto list = f.client->request(Method::Propfind, "/webdav/files/");
|
||||
check(list.status == 207, "propfind is 207");
|
||||
check(contains(list.body, "welcome.txt"), "seed file listed");
|
||||
auto put = f.client->request(Method::Put, "/webdav/files/new.txt",
|
||||
"new-data", {{"Content-Type", "text/plain"}});
|
||||
check(put.status == 201, "put is 201");
|
||||
auto get = f.client->request(Method::Get, "/webdav/files/new.txt");
|
||||
check(contains(get.body, "new-data"), "round trip works");
|
||||
auto del =
|
||||
f.client->request(Method::Delete, "/webdav/files/new.txt");
|
||||
check(del.status == 200, "delete is 200");
|
||||
auto gone = f.client->request(Method::Get, "/webdav/files/new.txt");
|
||||
check(gone.status == 404, "deleted file is 404");
|
||||
return finish("webdav/files");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Route: POST /workflow/release (bump, changelog, verify demo)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto feat = f.client->post("/workflow/release",
|
||||
"commits=feat(routing): add hello",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(feat.status == 200, "status 200");
|
||||
check(contains(feat.body, "\"bump\":\"minor\""), "feat maps to minor");
|
||||
check(contains(feat.body, "\"version\":\"0.2.0\""), "version bumped");
|
||||
check(contains(feat.body, "\"green\":true"), "checks green");
|
||||
auto breaking = f.client->post(
|
||||
"/workflow/release", "commits=feat!: drop old api",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(contains(breaking.body, "\"bump\":\"major\""),
|
||||
"bang maps to major");
|
||||
auto bad = f.client->post("/workflow/release", "commits=nope",
|
||||
"application/x-www-form-urlencoded");
|
||||
check(bad.status == 400, "malformed commit rejected");
|
||||
return finish("workflow/release");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Route: /ws/chat (websocket skeleton endpoint)
|
||||
#include "support/Assert.hpp"
|
||||
|
||||
using namespace molo::example::test;
|
||||
|
||||
int main() {
|
||||
Fixture f;
|
||||
auto info = f.client->get("/ws/chat");
|
||||
check(info.status == 200, "status 200");
|
||||
check(contains(info.body, "\"protocol\":\"websocket\""),
|
||||
"upgrade contract described");
|
||||
auto echo = f.client->post("/ws/chat", "ping", "text/plain");
|
||||
check(contains(echo.body, "\"echo\":\"ping\""), "frame echoed");
|
||||
return finish("ws/chat");
|
||||
}
|
||||
Reference in New Issue
Block a user