102 lines
5.4 KiB
C++
102 lines
5.4 KiB
C++
|
|
/*
|
||
|
|
catcrafts.net
|
||
|
|
Copyright (C) 2026 Catcrafts
|
||
|
|
|
||
|
|
The source code of this website is made available for viewing purposes only.
|
||
|
|
No permission is granted to copy, modify, distribute, or create derivative works.
|
||
|
|
*/
|
||
|
|
|
||
|
|
// Status codes, redirects, response headers, and the sitemap/feed documents —
|
||
|
|
// the things only a real request can show. A real 404 is the whole reason the
|
||
|
|
// backend exists: a client-side router cannot produce one.
|
||
|
|
|
||
|
|
import std;
|
||
|
|
import Catcrafts.E2eHarness;
|
||
|
|
|
||
|
|
using namespace Catcrafts::E2e;
|
||
|
|
|
||
|
|
int main(int argc, char** argv) {
|
||
|
|
TestServer srv(argv[1], 8210);
|
||
|
|
|
||
|
|
// ── status codes ──────────────────────────────────────────────────
|
||
|
|
for (std::string_view p : { "/", "/about", "/shop", "/shop/fp6-pmos", "/projects",
|
||
|
|
"/posts", "/demos", "/demos/raytracer", "/financials",
|
||
|
|
"/legal/privacy", "/legal/terms", "/legal/imprint",
|
||
|
|
"/feed.xml", "/sitemap.xml", "/api/healthz" }) {
|
||
|
|
srv.CheckStatus(std::string(p), "200");
|
||
|
|
}
|
||
|
|
// Trailing slashes must normalise, not 404 or duplicate the canonical URL.
|
||
|
|
srv.CheckStatus("/projects/", "200");
|
||
|
|
srv.CheckStatus("/shop/", "200");
|
||
|
|
// A real 404, which a client-side router cannot produce — this is the
|
||
|
|
// whole reason the backend exists.
|
||
|
|
srv.CheckStatus("/nope", "404");
|
||
|
|
srv.CheckStatus("/shop/nope", "404");
|
||
|
|
srv.CheckStatus("/legal/nope", "404");
|
||
|
|
// A slug that cannot be one of ours is rejected before any lookup.
|
||
|
|
srv.CheckStatus("/shop/BAD--slug", "404");
|
||
|
|
srv.CheckStatus("/demos/nope", "404");
|
||
|
|
// A post slug that parsed but names nothing must be a real 404, or every
|
||
|
|
// typo and every retired post becomes an indexable empty page.
|
||
|
|
srv.CheckStatus("/posts/nope", "404");
|
||
|
|
srv.CheckStatus("/posts/BAD--slug", "404");
|
||
|
|
// The retired blog URLs are still in the wild; they must redirect, not 404.
|
||
|
|
srv.CheckStatus("/blog", "301");
|
||
|
|
srv.CheckStatus("/blog/hello-world", "301");
|
||
|
|
// /demo was the single-demo URL before there was a list; it must redirect,
|
||
|
|
// not 404, because it was linked from the home page.
|
||
|
|
srv.CheckStatus("/demo", "301");
|
||
|
|
|
||
|
|
// ── redirects ─────────────────────────────────────────────────────
|
||
|
|
{
|
||
|
|
const auto blog = srv.Get("/blog/hello-world");
|
||
|
|
const auto it = blog.headers.find("location");
|
||
|
|
Check(it != blog.headers.end() && it->second.starts_with("/posts"),
|
||
|
|
"/blog/* sends Location: /posts",
|
||
|
|
it == blog.headers.end() ? "no location header" : it->second);
|
||
|
|
}
|
||
|
|
{
|
||
|
|
const auto demo = srv.Get("/demo");
|
||
|
|
const auto it = demo.headers.find("location");
|
||
|
|
Check(it != demo.headers.end() && it->second.starts_with("/demos"),
|
||
|
|
"/demo sends Location: /demos",
|
||
|
|
it == demo.headers.end() ? "no location header" : it->second);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ── headers ───────────────────────────────────────────────────────
|
||
|
|
srv.HeaderHas("/", "x-content-type-options", "nosniff", "nosniff on pages");
|
||
|
|
srv.HeaderHas("/", "cache-control", "public", "pages are cacheable");
|
||
|
|
srv.HeaderHas("/nope", "x-robots-tag", "noindex", "404 is noindex");
|
||
|
|
srv.HeaderHas("/feed.xml", "content-type", "application/atom", "feed content-type");
|
||
|
|
srv.HeaderHas("/sitemap.xml", "content-type", "application/xml", "sitemap content-type");
|
||
|
|
|
||
|
|
// ── sitemap and feed content ──────────────────────────────────────
|
||
|
|
srv.BodyHas("/sitemap.xml", "/shop/fp6-pmos", "sitemap lists the product");
|
||
|
|
srv.BodyHas("/sitemap.xml", "/about", "sitemap lists the about page");
|
||
|
|
srv.BodyHas("/sitemap.xml", "/legal/privacy", "sitemap lists the privacy page");
|
||
|
|
srv.BodyHas("/sitemap.xml", "/demos", "sitemap lists the demos page");
|
||
|
|
srv.BodyHas("/sitemap.xml", "/financials", "sitemap lists the financials page");
|
||
|
|
srv.BodyLacks("/sitemap.xml", "/blog", "sitemap does not advertise the redirect");
|
||
|
|
srv.BodyLacks("/sitemap.xml", "/order", "sitemap does not advertise order pages");
|
||
|
|
srv.BodyHas("/feed.xml", "<feed xmlns=\"http://www.w3.org/2005/Atom\">", "feed is Atom");
|
||
|
|
|
||
|
|
// ── abuse ─────────────────────────────────────────────────────────
|
||
|
|
srv.CheckStatus("/shop/fp6-pmos", "413", "POST",
|
||
|
|
"email=a%40b.example&name=" + std::string(20000, 'x')
|
||
|
|
+ "&street=x&postal=1&city=y&country=NL");
|
||
|
|
{
|
||
|
|
const auto json = srv.Post("/shop/fp6-pmos", "{}", "application/json");
|
||
|
|
Check(json.status == "415", "POST with a JSON content-type -> 415", json.status);
|
||
|
|
}
|
||
|
|
srv.CheckStatus("/shop/nope", "404", "POST",
|
||
|
|
"email=a%40b.example&name=Ada&street=x&postal=1&city=y&country=NL");
|
||
|
|
srv.CheckStatus("/projects", "405", "POST", "x=1");
|
||
|
|
// HEAD must not be a 500 or a body — some crawlers use it exclusively.
|
||
|
|
srv.CheckStatus("/", "200", "HEAD");
|
||
|
|
{
|
||
|
|
const auto head = srv.Head("/");
|
||
|
|
Check(head.body.empty(), "HEAD carries no body");
|
||
|
|
}
|
||
|
|
|
||
|
|
return Finish();
|
||
|
|
}
|