repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

3.9 KB · 113 lines · Go Raw History
  1package main
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"net/http/httptest"
  7	"strings"
  8	"testing"
  9
 10	"golang.org/x/net/html"
 11)
 12
 13// The three shapes instructions actually arrive in, plus an @graph wrapper,
 14// since a fixed lookup misses every site that uses one.
 15const recipePage = `<html><head>
 16<script type="application/ld+json">
 17{"@context":"https://schema.org","@graph":[
 18 {"@type":"WebPage","name":"not the recipe"},
 19 {"@type":["Recipe","Thing"],
 20  "name":"Breakfast Burritos",
 21  "recipeYield":["8","8 burritos"],
 22  "prepTime":"PT20M","cookTime":"PT1H15M","totalTime":"PT1H35M",
 23  "recipeIngredient":["8 large eggs","1/2 cup shredded cheddar","4 burrito-size flour tortillas","2 tbsp butter"],
 24  "recipeInstructions":[
 25    {"@type":"HowToSection","itemListElement":[
 26      {"@type":"HowToStep","text":"Beat the eggs with a pinch of salt."},
 27      {"@type":"HowToStep","text":"Melt the butter and scramble over low heat."}]},
 28    {"@type":"HowToStep","text":"Warm each tortilla until pliable, fill, and roll."}]}]}
 29</script></head><body><p>words words words</p></body></html>`
 30
 31func TestRecipeFromJSONLD(t *testing.T) {
 32	doc, err := html.Parse(strings.NewReader(recipePage))
 33	if err != nil {
 34		t.Fatal(err)
 35	}
 36	r, ok := recipeFromJSONLD(doc)
 37	if !ok {
 38		t.Fatal("no recipe found in a page that has one")
 39	}
 40	if r.Name != "Breakfast Burritos" {
 41		t.Errorf("name = %q", r.Name)
 42	}
 43	if r.Yield != "8" {
 44		t.Errorf("yield = %q", r.Yield)
 45	}
 46	if r.TotalTime != "1h 35m" {
 47		t.Errorf("total time = %q, want the ISO duration in words", r.TotalTime)
 48	}
 49	if r.CookTime != "1h 15m" {
 50		t.Errorf("cook time = %q", r.CookTime)
 51	}
 52	if r.PrepTime != "20 minutes" {
 53		t.Errorf("prep time = %q", r.PrepTime)
 54	}
 55	if len(r.Ingredients) != 4 || !strings.HasPrefix(r.Ingredients[0], "8 large eggs") {
 56		t.Errorf("ingredients = %v", r.Ingredients)
 57	}
 58	// The point of the whole file: the quantity survives.
 59	md := r.Markdown()
 60	for _, want := range []string{"8 large eggs", "1/2 cup shredded cheddar", "Makes: 8", "1. Beat the eggs"} {
 61		if !strings.Contains(md, want) {
 62			t.Errorf("markdown is missing %q:\n%s", want, md)
 63		}
 64	}
 65	// A section wrapping steps must flatten rather than disappear.
 66	if len(r.Steps) != 3 {
 67		t.Errorf("want 3 steps flattened out of the section, got %d: %v", len(r.Steps), r.Steps)
 68	}
 69}
 70
 71func TestRecipeIgnoresNonRecipe(t *testing.T) {
 72	doc, _ := html.Parse(strings.NewReader(
 73		`<html><head><script type="application/ld+json">{"@type":"Article","name":"x"}</script></head></html>`))
 74	if _, ok := recipeFromJSONLD(doc); ok {
 75		t.Error("claimed a recipe on an article page")
 76	}
 77}
 78
 79// TestFetchKeepsRecipe goes through the whole of Fetch rather than calling the
 80// parser directly, because the first version of this worked in isolation and
 81// did nothing in production: stripAndPick removes script tags in place, so
 82// reading the structured data after it ran found an empty document.
 83func TestFetchKeepsRecipe(t *testing.T) {
 84	page := `<html><head>
 85<script type="application/ld+json">
 86{"@type":"Recipe","name":"Breakfast Burritos","recipeYield":"8",
 87 "recipeIngredient":["8 large eggs","1/2 cup shredded cheddar","4 flour tortillas"],
 88 "recipeInstructions":[{"@type":"HowToStep","text":"Beat the eggs."},
 89                       {"@type":"HowToStep","text":"Scramble and roll."}]}
 90</script></head><body><article>` +
 91		strings.Repeat("Some prose about burritos that mentions eggs and cheese but no amounts. ", 12) +
 92		`</article></body></html>`
 93
 94	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 95		w.Header().Set("Content-Type", "text/html; charset=utf-8")
 96		fmt.Fprint(w, page)
 97	}))
 98	defer srv.Close()
 99
100	p, err := Fetch(srv.Client(), srv.URL)
101	if err != nil {
102		t.Fatal(err)
103	}
104	if !hasStructuredRecipe(p) {
105		t.Fatalf("the recipe block is not at the top of the markdown:\n%.300s", p.Markdown)
106	}
107	for _, want := range []string{"8 large eggs", "1/2 cup shredded cheddar", "Makes: 8"} {
108		if !strings.Contains(p.Markdown, want) {
109			t.Errorf("markdown lost %q", want)
110		}
111	}
112}