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 }