catcrafts.net/tests/ShouldEmitStructuredData/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

175 lines
7.8 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.
*/
// The JSON-LD blocks are the machine-readable identity and offer records,
// born from a Google AI overview flatly asserting that nobody named Catcrafts
// sells Fairphone hardware. Parsed with our own JSON reader — the same one
// that must accept them — and checked for the facts.
import std;
import Catcrafts.Shared;
import Catcrafts.E2eHarness;
using namespace Catcrafts;
using namespace Catcrafts::E2e;
namespace {
// Every <script type="application/ld+json"> block on the page, parsed. The
// block content is pure JSON, which contains no '<', so slicing to the next
// tag is exact.
std::vector<Json::Value> ExtractLd(TestServer& srv, const std::string& path) {
const std::string body = srv.Body(path);
constexpr std::string_view kOpen = "<script type=\"application/ld+json\">";
std::vector<Json::Value> out;
for (std::size_t pos = body.find(kOpen); pos != std::string::npos;
pos = body.find(kOpen, pos + kOpen.size())) {
const std::size_t start = pos + kOpen.size();
const std::size_t end = body.find('<', start);
if (end == std::string::npos) break;
if (auto v = Json::Parse(std::string_view(body).substr(start, end - start))) {
out.push_back(std::move(*v));
}
}
return out;
}
} // namespace
int main(int argc, char** argv) {
TestServer srv(argv[1], 8212);
// ── the home identity graph ───────────────────────────────────────
// The home record is an @graph (Organization + WebSite joined by @id);
// the org node inside it must carry the registered identity.
{
bool ok = false;
for (const Json::Value& doc : ExtractLd(srv, "/")) {
const Json::Value* g = doc.Find("@graph");
if (!g || !g->IsArray()) continue;
std::size_t orgs = 0;
for (const Json::Value& node : g->array) {
if (node.Str("@type") == "Organization"
&& node.Str("vatID") == "NL003329281B38") {
++orgs;
}
}
ok = ok || orgs == 1;
}
Check(ok, "home Organization schema parses and carries the VAT identity");
}
// ── the product record ────────────────────────────────────────────
// Variants are a ProductGroup: one variant Product per colour, each with
// its own single offer — not one Product with three prices.
const std::vector<Json::Value> productLd = ExtractLd(srv, "/shop/fp6-pmos");
const Json::Value* group = nullptr;
for (const Json::Value& doc : productLd) {
if (doc.Str("@type") == "ProductGroup") group = &doc;
}
{
bool variantsOk = false;
if (group) {
if (const Json::Value* v = group->Find("hasVariant"); v && v->IsArray()
&& v->array.size() == 3) {
variantsOk = true;
for (const Json::Value& node : v->array) {
const Json::Value* offers = node.Find("offers");
variantsOk = variantsOk && offers && offers->IsObject();
}
}
}
Check(variantsOk, "product schema parses with one variant (and offer) per colour");
}
{
bool listOk = false;
for (const Json::Value& doc : ExtractLd(srv, "/shop")) {
if (doc.Str("@type") == "ItemList") {
const Json::Value* items = doc.Find("itemListElement");
listOk = items && items->IsArray() && !items->array.empty();
}
}
Check(listOk, "shop index carries an ItemList of the product pages");
}
srv.BodyHas("/shop/fp6-pmos", "\"price\":\"563.30\"", "schema price is the checkout integer");
// Merchant-grade offer fields: what Merchant Center's website-crawl feed
// reads. Shipping is published from the live carrier table at one unit's
// weight — the same integers checkout charges, so the listing cannot
// quote a rate the till won't honour; returns mirror the terms page.
srv.BodyHas("/shop/fp6-pmos", "OfferShippingDetails", "offers carry shipping details");
srv.BodyHas("/shop/fp6-pmos", "MerchantReturnPolicy", "offers carry a return policy");
srv.BodyHas("/shop/fp6-pmos", "\"sku\":\"fp6-pmos-green\"", "offers carry per-variant skus");
srv.BodyHas("/shop/fp6-pmos", "\"brand\":{\"@type\":\"Brand\",\"name\":\"Fairphone\"}",
"product carries the hardware brand");
// One entry per (transit tier, price) the carrier table produces — three,
// for the fixture's NL/DE/GB. Not a fixed property of the code any more:
// it is whatever the carrier prices, which is the point.
const Json::Value* shipping = nullptr;
if (group) {
if (const Json::Value* v = group->Find("hasVariant"); v && v->IsArray()
&& !v->array.empty()) {
if (const Json::Value* offers = v->array[0].Find("offers")) {
shipping = offers->Find("shippingDetails");
}
}
}
Check(shipping && shipping->IsArray() && shipping->array.size() == 3,
"shipping details group the carrier's rates");
// The advertised rate IS the carrier's single-unit price, and a
// destination the table does not cover is never advertised.
if (shipping && shipping->IsArray()) {
std::vector<std::string> rates;
bool au = false;
for (const Json::Value& detail : shipping->array) {
if (const Json::Value* rate = detail.Find("shippingRate")) {
rates.emplace_back(rate->Str("value"));
}
if (const Json::Value* dest = detail.Find("shippingDestination")) {
if (const Json::Value* cc = dest->Find("addressCountry"); cc && cc->IsArray()) {
for (const Json::Value& c : cc->array) au = au || c.string == "AU";
}
}
}
std::ranges::sort(rates);
Check(rates == std::vector<std::string>{ "15.00", "25.00", "55.00" },
"published shipping rates come from the carrier table");
Check(!au, "an uncovered destination is not advertised");
}
if (srv.ShopOpen()) {
srv.BodyHas("/shop/fp6-pmos", "schema.org/InStock",
"open shop maps to InStock availability");
} else {
srv.BodyHas("/shop/fp6-pmos", "schema.org/PreOrder",
"coming-soon maps to PreOrder availability");
}
// og: tags are the link-preview card on Mastodon and Lemmy — where the
// traffic actually comes from.
srv.BodyHas("/", "property=\"og:title\"", "home page has og:title");
srv.BodyHas("/shop/fp6-pmos", "og:image\" content=\"https://catcrafts.net/fp6-pmos.jpg",
"product og:image is the absolute photo URL");
// ── the person-company weld ───────────────────────────────────────
// The about page: the founder must be named in the HTML, the Person
// schema must parse, and the home page byline must link it.
srv.BodyHas("/about", "Jorijn van der Graaf", "about page names the founder");
srv.BodyHas("/", "Jorijn van der Graaf", "home page carries the founder byline");
{
bool personOk = false;
for (const Json::Value& doc : ExtractLd(srv, "/about")) {
if (const Json::Value* person = doc.Find("mainEntity")) {
personOk = personOk || person->Str("name") == "Jorijn van der Graaf";
}
}
Check(personOk, "about Person schema parses and names the founder");
}
return Finish();
}