catcrafts.net/tests/ShouldServeRoutes/main.cpp
Jorijn van der Graaf 25805a093a
All checks were successful
Deploy / build-deploy (push) Successful in 5m35s
removed fp6 from shop
2026-09-05 18:38:39 +02:00

116 lines
6.1 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.Shared;
import Catcrafts.E2eHarness;
using namespace Catcrafts;
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/donation", "/projects",
"/posts", "/demos", "/demos/raytracer", "/financials",
"/legal/privacy", "/legal/terms", "/legal/imprint",
"/feed.xml", "/sitemap.xml", "/api/healthz" }) {
srv.CheckStatus(std::string(p), "200");
}
// Every catalogue entry has a page, whatever its status: a coming-soon or
// withdrawn listing stays up with its form closed, and only a slug the
// catalogue never held is a 404.
for (const Product& pr : Content::Products()) {
srv.CheckStatus("/shop/" + pr.slug, "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 ──────────────────────────────────────
// Product URLs come off the catalogue, not a second list, so every entry
// is advertised and nothing that is not listed can be.
for (const Product& pr : Content::Products()) {
srv.BodyHas("/sitemap.xml", "/shop/" + pr.slug, "sitemap lists " + pr.slug);
}
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 ─────────────────────────────────────────────────────────
// The body guards run for any listed product before its form is read, so
// the donation item (the one entry every catalogue has) is the target.
srv.CheckStatus("/shop/donation", "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/donation", "{}", "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();
}