catcrafts.net/tests/ShouldServePostPages/main.cpp
Jorijn van der Graaf 749f525f83
All checks were successful
Deploy / build-deploy (push) Successful in 4m19s
tests and eurc
2026-08-15 00:54:05 +02:00

245 lines
13 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.
*/
// Post media and post pages over real HTTP. The media IS the content of these
// posts (screen recordings of the work), and it must come from our own
// origin: the privacy notice states that everything the browser loads comes
// from catcrafts.net, and a third-party embed would send every visitor's IP
// to whichever instance hosted the file.
import std;
import Catcrafts.E2eHarness;
using namespace Catcrafts::E2e;
namespace {
// Media loaded from a third party — `poster` is in the list because a video
// poster is fetched on page load exactly like an <img> src is.
const std::regex kThirdPartyMedia(
R"((src|srcset|href|poster)="https?://[^"]*\.(mp4|webm|webp|avif|png|jpe?g|gif))");
std::vector<std::string> PostPathsFromSitemap(TestServer& srv, std::size_t limit) {
const std::string sitemap = srv.Body("/sitemap.xml");
const std::regex slug(R"(/posts/[a-z0-9-]+)");
std::vector<std::string> out;
for (auto it = std::sregex_iterator(sitemap.begin(), sitemap.end(), slug);
it != std::sregex_iterator() && out.size() < limit; ++it) {
if (std::ranges::find(out, it->str()) == out.end()) out.push_back(it->str());
}
return out;
}
} // namespace
int main(int argc, char** argv) {
TestServer srv(argv[1], 8214);
const std::string posts = srv.Body("/posts");
// ── post media ────────────────────────────────────────────────────
Check(std::regex_search(posts, std::regex(R"(<(img|video) class="post-media__item")")),
"/posts embeds its media");
Check(!std::regex_search(posts, kThirdPartyMedia),
"/posts loads no media from a third party");
// Dimensions prevent layout shift as each file arrives. Needs ffprobe at
// fetch time — a build host without it produces no dimensions at all,
// which is what this catches.
Check(std::regex_search(posts, std::regex(
R"(<img class="post-media__item"[^>]*width="[0-9]+" height="[0-9]+")")),
"images carry width/height");
// Videos too. This assertion exists because they silently lost theirs:
// ffprobe appends an empty CSV field for some files, so parsing
// `width,height` as one joined string yielded a height of "480x" and the
// guard discarded both.
Check(std::regex_search(posts, std::regex(
R"(<video class="post-media__item"[^>]*width="[0-9]+" height="[0-9]+")")),
"videos carry width/height");
// A poster is the frame shown before anyone presses play, and these posts
// ARE their video. "At least one" rather than "every one": an instance
// that generated no thumbnail is a legitimate empty poster, but zero
// posters across every video means the fetch/mirror/render chain broke.
Check(std::regex_search(posts, std::regex(
R"(<video class="post-media__item"[^>]*poster="/media/)")),
"videos carry a locally-hosted poster");
// A video offering an AV1 <source> must offer an H.264 one after it: the
// codecs parameter is what lets a browser without AV1 skip to a file it
// can play. Conditional — a build whose posts carry no AV1 has nothing to
// check.
if (posts.find("codecs=av01") != std::string::npos) {
Check(std::regex_search(posts, std::regex(
R"(<source src="/media/[^"]*\.h264\.mp4" type="video/mp4">)")),
"AV1 videos carry an H.264 fallback source");
}
// preload="metadata", not auto: several 5 MB recordings must not all
// download on page load.
Check(posts.find("preload=\"metadata\"") != std::string::npos,
"video does not preload its whole body");
// ── post pages ────────────────────────────────────────────────────
// The post page is where the body lives, and the body is the reason the
// site has anything for a search engine to index beyond a list of links
// off it. Its slug is data, so take one from the page rather than
// hardcoding a title that will be wrong the week after it is written.
std::string postPath;
{
std::smatch m;
if (std::regex_search(posts, m, std::regex(R"lit(href="(/posts/[a-z0-9-]+)")lit"))) {
postPath = m[1].str();
}
}
Check(!postPath.empty(), "/posts links a post page; a body exists somewhere");
if (!postPath.empty()) {
srv.CheckStatus(postPath, "200");
srv.BodyHas("/posts", "Read the full post", "/posts offers the full post");
// And it trails the excerpt, immediately after the ellipsis the
// truncation left, rather than sitting as its own row below the
// media. The excerpt is escaped text, so nothing but the link can put
// a '<' between the two.
Check(std::regex_search(posts, std::regex(
R"(<p class="post-card__excerpt">[^<]*<a class="link-more" href="/posts/)")),
"read-more trails the excerpt");
const std::string page = srv.Body(postPath);
Check(page.find("<div class=\"post-body\">") != std::string::npos,
"post page carries the rendered body");
// Rendered, not dumped: a body that reached the page as literal
// Markdown would show its own asterisks and hashes to the reader and
// to a crawler.
Check(std::regex_search(page, std::regex(R"(<(p|h2|h3|h4|ul|ol|blockquote|pre)>)")),
"post body is real markup, not literal Markdown");
// The canonical points here, not at the instance. That is the entire
// SEO argument for hosting the body: two copies of the text exist,
// and this says which one is the original as far as this site is
// concerned.
Check(page.find("rel=\"canonical\" href=\"https://catcrafts.net/posts/")
!= std::string::npos,
"post page is its own canonical");
Check(page.find("\"@type\":\"BlogPosting\"") != std::string::npos,
"post page carries BlogPosting JSON-LD");
Check(page.find("\"@id\":\"https://catcrafts.net/#organization\"") != std::string::npos,
"post JSON-LD joins the organization node");
Check(page.find("\"@id\":\"https://catcrafts.net/about#person\"") != std::string::npos,
"post JSON-LD joins the founder node");
Check(page.find("property=\"og:type\" content=\"article\"") != std::string::npos,
"post page is an article to og:");
// Hosting the body does not mirror the discussion; the thread is
// still one click away and is still where the comments are.
Check(std::regex_search(page, std::regex(R"lit(href="https://[a-z0-9.-]+/post/[0-9]+")lit")),
"post page still links its thread");
// The body is prose, not an application.
Check(page.find("<script>") == std::string::npos,
"post page ships no executable script");
Check(page.find("<base") == std::string::npos, "post page has no base tag");
// Every inline image and video a body embeds is mirrored, exactly
// like a card's media — "everything comes from catcrafts.net" covers
// href as well as src, so a body linking a .webp on someone else's
// instance is the same leak as embedding one.
const std::vector<std::string> pages = PostPathsFromSitemap(srv, 20);
std::vector<std::string> referencedMedia;
for (const std::string& pg : pages) {
const std::string body = srv.Body(pg);
Check(!std::regex_search(body, kThirdPartyMedia),
std::format("{} loads no media from a third party", pg));
const std::regex mediaRef(R"lit((src|srcset)="(/media/[^"]+)")lit");
for (auto it = std::sregex_iterator(body.begin(), body.end(), mediaRef);
it != std::sregex_iterator(); ++it) {
const std::string f = (*it)[2].str();
if (std::ranges::find(referencedMedia, f) == referencedMedia.end()) {
referencedMedia.push_back(f);
}
}
}
// The image format ladder: AVIF first, the mirrored original next,
// and a PNG on the <img> underneath, so exactly one file is fetched
// and every browser can read one of them. Order is the whole point —
// a browser takes the first source it understands.
if (page.find("<picture>") != std::string::npos) {
Check(std::regex_search(page, std::regex(
R"(<picture><source srcset="/media/[^"]+\.avif" type="image/avif">)")),
"inline images lead with an AVIF source");
Check(std::regex_search(page, std::regex(
R"lit(<img class="post-media__item"[^>]*src="/media/[^"]+\.png")lit")),
"inline images fall back to a PNG the img itself points at");
// Every tier has to be a file that exists, or the ladder serves a
// 404 to whichever browsers pick that rung — precisely the set of
// browsers nobody testing this site is using. The files are
// served by Caddy rather than by this server, so they are checked
// on disk. NOT hardcoded to ./media: CI points the mirror at the
// persistent mount instead (E2E_MEDIA_DIR) so the copies survive
// a deploy.
std::filesystem::path mediaDir;
if (const char* env = std::getenv("E2E_MEDIA_DIR"); env && *env) {
mediaDir = env;
} else {
for (std::string_view d : { "media", "/deploy-app/media" }) {
if (std::filesystem::is_directory(d)) { mediaDir = d; break; }
}
}
if (mediaDir.empty()) {
std::println("note: no media directory found; set E2E_MEDIA_DIR — "
"the media-files-exist check did not run");
} else {
std::size_t missing = 0;
for (const std::string& f : referencedMedia) {
if (!std::filesystem::exists(mediaDir / f.substr(std::string_view("/media/").size()))) {
++missing;
// Bounded: a wrong directory makes EVERY file missing,
// and a hundred identical lines buries the one fact
// that matters.
if (missing <= 5) std::println(std::cerr, " missing: {}", f);
}
}
Check(missing == 0,
std::format("every referenced media file is in {}", mediaDir.string()),
std::format("{} referenced file(s) missing", missing));
}
} else {
std::println("note: no <picture> on {} — ffmpeg absent at mirror time? "
"the format-ladder checks did not run", postPath);
}
// Inline screenshots get dimensions from the sidecar list
// fetch-media.sh writes, because Markdown syntax has nowhere to carry
// them. Conditional: a post whose body embeds nothing has nothing to
// check.
if (page.find("<img class=\"post-media__item\"") != std::string::npos) {
Check(std::regex_search(page, std::regex(
R"(<img class="post-media__item"[^>]*width="[0-9]+" height="[0-9]+")")),
"inline body images carry width/height");
}
}
// The sitemap has to advertise the pages, or hosting the bodies buys
// nothing.
Check(std::regex_search(srv.Body("/sitemap.xml"), std::regex(
R"(<loc>https://catcrafts\.net/posts/[a-z0-9-]+</loc>)")),
"sitemap lists the post pages");
// Every outbound thread link is a real permalink: absolute https, on some
// instance, pointing at a numeric post id. A resolution failure
// legitimately falls back to the author's copy, so this checks the shape
// rather than naming a host.
{
const std::regex permalink(R"lit(href="https://[a-z0-9.-]+/post/[0-9]+")lit");
std::size_t links = 0;
for (auto it = std::sregex_iterator(posts.begin(), posts.end(), permalink);
it != std::sregex_iterator(); ++it) {
++links;
}
Check(links > 0, "posts list links its threads by permalink");
// Nothing should link a post by a bare id or a relative path — that
// would mean a permalink was rendered without its origin and silently
// resolves to catcrafts.net.
Check(!std::regex_search(posts, std::regex(R"lit(href="/post/[0-9]+")lit")),
"no thread link resolves to catcrafts.net");
}
return Finish();
}