CI / ci (push) Successful in 9m39s
Co-authored-by: orchestrator-agent <20+orchestrator-agent@code.cubecraftcreations.com>
186 lines
6.5 KiB
Go
186 lines
6.5 KiB
Go
package dinners_test
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"code.cubecraftlabs.com/Maniacs_Mansion/Meal-Quest/internal/dinners"
|
|
)
|
|
|
|
// TestCreateRegularNeverNamesFromLink: a Regular added with a recipe link
|
|
// is named only by the household, so no part of the link — host, path,
|
|
// query or fragment — is stored as its name or shown in a dinner or a
|
|
// duplicate error, even a token of plain letters in the path.
|
|
func TestCreateRegularNeverNamesFromLink(t *testing.T) {
|
|
t.Parallel()
|
|
e := newUnstartedEnv(t)
|
|
const link = "https://c.example/stew-key-abcdefghijklmno?sig=zyxwvut#lookup"
|
|
// Each leak holds a character no ULID has (Crockford base32 has no I, L,
|
|
// O, U, "-", "." or "/"), so a dinner's id never matches one.
|
|
leaks := []string{link, "c.example", "stew-key", "abcdefghijklmno", "sig", "zyxwvut", "lookup"}
|
|
check := func(what, text string) {
|
|
t.Helper()
|
|
lower := strings.ToLower(text)
|
|
for _, leak := range leaks {
|
|
if strings.Contains(lower, leak) {
|
|
t.Errorf("%s contains %q: %s", what, leak, text)
|
|
}
|
|
}
|
|
}
|
|
|
|
_, err := e.svc.CreateRegular(t.Context(), dinners.NewRegular{RecipeURL: ptr(link)}, member)
|
|
var verr *dinners.ValidationError
|
|
if !errors.As(err, &verr) {
|
|
t.Fatalf("link without a name: err = %v, want a *ValidationError", err)
|
|
}
|
|
check("validation error", verr.Error())
|
|
|
|
c, err := e.svc.CreateRegular(t.Context(), dinners.NewRegular{Name: ptr(" Sunday stew "), RecipeURL: ptr(link)}, member)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Name != "Sunday stew" {
|
|
t.Errorf("name = %q, want the household's %q", c.Name, "Sunday stew")
|
|
}
|
|
created, err := json.Marshal(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check("create response", string(created))
|
|
row := e.dinner(t, c.ID)
|
|
check("stored name", row.Name+" "+row.NormalizedName)
|
|
if row.RecipeUrl.String != link {
|
|
t.Errorf("stored recipe_url = %q, want %q kept for grounding", row.RecipeUrl.String, link)
|
|
}
|
|
|
|
list, err := e.svc.List(t.Context(), dinners.StatusRegular)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listed, err := json.Marshal(list)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check("list response", string(listed))
|
|
|
|
// The same link under another name is still a duplicate, and its error
|
|
// names the existing dinner by the household's name.
|
|
_, err = e.svc.CreateRegular(t.Context(), dinners.NewRegular{Name: ptr("Weeknight chili"), RecipeURL: ptr(link)}, member)
|
|
var dup *dinners.DuplicateError
|
|
if !errors.As(err, &dup) || dup.Existing.ID != c.ID {
|
|
t.Fatalf("same link again: err = %v, want a duplicate of %s", err, c.ID)
|
|
}
|
|
check("duplicate error", dup.Error())
|
|
}
|
|
|
|
// TestSourceOfSiteName: a site name the page gave is shown as it stands,
|
|
// even one shaped like a host and even over a link that is withheld; a
|
|
// recipe whose page named none is named by its link's host without "www."
|
|
// only when the link itself is shown, and is unnamed otherwise.
|
|
func TestSourceOfSiteName(t *testing.T) {
|
|
t.Parallel()
|
|
const (
|
|
ordinary = "https://www.Kitchen.example/recipes/stew"
|
|
token = "https://private-token.example.com/recipes/stew?access_token=abc123"
|
|
)
|
|
for _, tc := range []struct {
|
|
stored string
|
|
valid bool
|
|
link string
|
|
want, wantURL *string
|
|
}{
|
|
{"Delish", true, ordinary, ptr("Delish"), ptr(ordinary)},
|
|
{"Food.com", true, ordinary, ptr("Food.com"), ptr(ordinary)},
|
|
{"Bon Appétit", true, token, ptr("Bon Appétit"), nil},
|
|
{"", false, ordinary, ptr("Kitchen.example"), ptr(ordinary)},
|
|
{"", true, ordinary, ptr("Kitchen.example"), ptr(ordinary)},
|
|
{"", false, token, nil, nil},
|
|
{"", false, "https://203.0.113.7/stew", ptr("203.0.113.7"), ptr("https://203.0.113.7/stew")},
|
|
{"", false, "http://kitchen.example/stew", nil, nil},
|
|
} {
|
|
src, err := dinners.SourceOf(sql.NullString{String: tc.stored, Valid: tc.valid}, tc.link)
|
|
if err != nil {
|
|
t.Fatalf("SourceOf(%q, %q): %v", tc.stored, tc.link, err)
|
|
}
|
|
if got := src.SiteName; (got == nil) != (tc.want == nil) || (got != nil && *got != *tc.want) {
|
|
t.Errorf("SourceOf(%q, %q) site name = %v, want %v", tc.stored, tc.link, deref(got), deref(tc.want))
|
|
}
|
|
if got := src.URL; (got == nil) != (tc.wantURL == nil) || (got != nil && *got != *tc.wantURL) {
|
|
t.Errorf("SourceOf(%q, %q) url = %v, want %v", tc.stored, tc.link, deref(got), deref(tc.wantURL))
|
|
}
|
|
}
|
|
// A link that does not parse is an error with or without a site name.
|
|
for _, site := range []sql.NullString{{String: "Delish", Valid: true}, {}} {
|
|
if _, err := dinners.SourceOf(site, "https://secret-token\x7f.example/%zz"); err == nil || strings.Contains(err.Error(), "secret-token") {
|
|
t.Errorf("SourceOf(%v) of an unparseable link: err = %v, want one not quoting the link", site, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSafeSourceURLOnlyWithoutSecrets: a recipe link is shown only when it
|
|
// is https to a host with no user, password, query or fragment, each of
|
|
// which can carry a token; any other link, or one that does not parse, is
|
|
// withheld whole.
|
|
func TestSafeSourceURLOnlyWithoutSecrets(t *testing.T) {
|
|
t.Parallel()
|
|
for _, link := range []string{
|
|
"https://www.example.com/recipes/chicken-curry",
|
|
"https://example.com",
|
|
"https://example.com:8443/stew/",
|
|
"https://[2001:db8::1]/curry",
|
|
"https://1.2.3.4/curry",
|
|
"https://Kitchen-2.Example.com./curry",
|
|
} {
|
|
if got := dinners.SafeSourceURL(link); got == nil || *got != link {
|
|
t.Errorf("SafeSourceURL(%q) = %v, want the link", link, deref(got))
|
|
}
|
|
}
|
|
for _, link := range []string{
|
|
"https://example.com/curry?access_token=query-token",
|
|
"https://example.com/curry?",
|
|
"https://user:password@example.com/curry",
|
|
"https://token@example.com/curry",
|
|
"https://@example.com/curry",
|
|
"http://example.com/curry",
|
|
"https://example.com/curry#token=fragment-token",
|
|
"https://example.com/curry#",
|
|
"ftp://example.com/curry",
|
|
"https:example.com/curry",
|
|
"https:///curry",
|
|
"https://example.com:99999/curry",
|
|
"https://example.com:0/curry",
|
|
"//example.com/curry",
|
|
"https://secret-token\x7f.example/%zz",
|
|
"",
|
|
// Hosts url.Parse accepts but the browser's URL parser refuses
|
|
// (or reads as another host), which would fail the whole recipe
|
|
// screen.
|
|
"https://[bad]/curry",
|
|
"https://[fe80::1%25en0]/curry",
|
|
"https://[1.2.3.4]/curry",
|
|
"https://1.2.3.999/curry",
|
|
"https://1.2.3.4.5/curry",
|
|
"https://999/curry",
|
|
"https://example.0x/curry",
|
|
"https://xn--a/curry",
|
|
"https://ex%61mple.com/curry",
|
|
"https://ex%2fample.com/curry",
|
|
"https://a..b/curry",
|
|
"https://café.example/curry",
|
|
} {
|
|
if got := dinners.SafeSourceURL(link); got != nil {
|
|
t.Errorf("SafeSourceURL(%q) = %q, want nil", link, *got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func deref(s *string) any {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
return *s
|
|
}
|