package main import ( "context" "encoding/json" "image" "image/png" "net/http" "net/http/httptest" "net/netip" "net/url" "os" "strings" "testing" ) func TestPreviewKey(t *testing.T) { yt := "https://www.youtube.com/watch?v=dQw4w9WgXcQ" cases := map[string]string{ "guck mal https://youtu.be/dQw4w9WgXcQ?t=42": yt, "https://www.youtube.com/watch?v=dQw4w9WgXcQ&list=x": yt, "https://m.youtube.com/watch?feature=share&v=dQw4w9WgXcQ": yt, "https://youtube.com/shorts/dQw4w9WgXcQ": yt, "https://music.youtube.com/watch?v=dQw4w9WgXcQ": yt, "https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ": yt, "https://www.youtube.com/@kanal": "https://www.youtube.com/@kanal", "https://www.youtube.com/watch?v=kurz": "https://www.youtube.com/watch?v=kurz", "erst https://example.org/a dann https://youtu.be/dQw4w9WgXcQ": "https://example.org/a", "ohne Link": "", "ftp://example.org": "", "https://example.org/x danach": "https://example.org/x", "https://example.org/": "https://example.org/", } for in, want := range cases { if got := previewKey(in); got != want { t.Errorf("previewKey(%q) = %q, erwartet %q", in, got, want) } } if got := previewKey("https://example.org/" + strings.Repeat("a", maxPreviewURL)); got != "" { t.Errorf("überlange URL ergab %q", got) } } func TestIsPublicIP(t *testing.T) { cases := map[string]bool{ "93.184.216.34": true, "2a00:1450:4001::200e": true, "127.0.0.1": false, "10.1.2.3": false, "172.16.0.1": false, "192.168.161.4": false, "169.254.169.254": false, // Cloud-Metadaten "100.64.0.1": false, "0.1.2.3": false, "0.0.0.0": false, "255.255.255.255": false, "::1": false, "fd00::1": false, "fe80::1": false, "::ffff:127.0.0.1": false, "::ffff:93.184.216.34": true, "64:ff9b::a00:1": false, "2002:c0a8:101::1": false, "224.0.0.1": false, } for in, want := range cases { if got := isPublicIP(netip.MustParseAddr(in)); got != want { t.Errorf("isPublicIP(%s) = %v, erwartet %v", in, got, want) } } } // Der echte Wächter muss interne Ziele abweisen, ohne dass ein Request das // Netz verlässt -- auch nach einer Weiterleitung. func TestPreviewClientBlocksInternal(t *testing.T) { internal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Error("interner Server wurde erreicht") })) defer internal.Close() if _, err := previewGet(context.Background(), internal.URL, "text/html"); err == nil { t.Fatal("Abruf von 127.0.0.1 wurde nicht blockiert") } if err := previewDialGuard("8.8.8.8:5432"); err == nil { t.Error("Port 5432 wurde nicht blockiert") } } func TestParsePageMeta(t *testing.T) { doc := ` Seiten titel ` m := parsePageMeta(strings.NewReader(doc)) want := map[string]string{ "title": "Seiten titel", "og:title": "OG-Titel", "twitter:image": "/bild.png", "og:site_name": "Beispiel", } for k, v := range want { if m[k] != v { t.Errorf("%s = %q, erwartet %q", k, m[k], v) } } if _, ok := m["og:image"]; ok { t.Error("Meta aus dem body wurde übernommen") } } // fakeRemote stellt eine Seite mit og-Tags, ein Bild und einen oEmbed- // Endpunkt bereit und lenkt die Vorschau-Abrufe für die Testdauer dorthin. func fakeRemote(t *testing.T) *httptest.Server { t.Helper() var buf strings.Builder img := image.NewRGBA(image.Rect(0, 0, 1280, 720)) png.Encode(&buf, img) pngData := buf.String() mux := http.NewServeMux() mux.HandleFunc("/seite", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(` `)) }) mux.HandleFunc("/umleitung", func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/seite", http.StatusFound) }) mux.HandleFunc("/ohne-titel", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") w.Write([]byte(`nix`)) }) mux.HandleFunc("/bild.png", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.Write([]byte(pngData)) }) mux.HandleFunc("/oembed", func(w http.ResponseWriter, r *http.Request) { u, _ := url.Parse(r.URL.Query().Get("url")) if u == nil || u.Query().Get("v") != "dQw4w9WgXcQ" { http.Error(w, "Not Found", http.StatusNotFound) return } json.NewEncoder(w).Encode(map[string]string{"title": "Ein Video", "author_name": "Ein Kanal"}) }) mux.HandleFunc("/vi/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png") w.Write([]byte(pngData)) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) oldGuard, oldOEmbed, oldThumbs := previewDialGuard, youtubeOEmbed, youtubeThumbs previewDialGuard = func(string) error { return nil } youtubeOEmbed = srv.URL + "/oembed" // Erster Kandidat fehlt -> der nächste greift. youtubeThumbs = []string{srv.URL + "/fehlt/%s.jpg", srv.URL + "/vi/%s/hqdefault.jpg"} t.Cleanup(func() { previewDialGuard, youtubeOEmbed, youtubeThumbs = oldGuard, oldOEmbed, oldThumbs }) // Bilder landen relativ zum CWD unter static/media/preview. dir := t.TempDir() wd, _ := os.Getwd() if err := os.Chdir(dir); err != nil { t.Fatal(err) } t.Cleanup(func() { os.Chdir(wd) }) return srv } func TestFetchPage(t *testing.T) { remote := fakeRemote(t) p, img, err := fetchPage(context.Background(), remote.URL+"/umleitung") if err != nil { t.Fatal(err) } if p.Title != "Ein Artikel" || p.Site != "127.0.0.1" || p.URL != remote.URL+"/umleitung" { t.Errorf("unerwartete Vorschau %+v", p) } // Relatives Bild gegen die Adresse NACH der Weiterleitung aufgelöst. if len(img) != 1 || img[0] != remote.URL+"/bild.png" { t.Errorf("bild = %q", img) } if _, _, err := fetchPage(context.Background(), remote.URL+"/ohne-titel"); err == nil { t.Error("Seite ohne Titel ergab eine Vorschau") } if _, _, err := fetchPage(context.Background(), remote.URL+"/bild.png"); err == nil { t.Error("Bild als Seite ergab eine Vorschau") } name := thumbPath("x") if err := storeThumb(context.Background(), img[0], name); err != nil { t.Fatal(err) } f, err := os.Open(name) if err != nil { t.Fatal(err) } defer f.Close() cfg, format, err := image.DecodeConfig(f) if err != nil || format != "jpeg" || cfg.Width != thumbSize || cfg.Height != 180 { t.Errorf("thumb: %s %dx%d %v", format, cfg.Width, cfg.Height, err) } } // Ende-zu-Ende: Beitrag mit YouTube-Link anlegen, Vorschau holen, im Feed // sehen; Bearbeiten ohne Link nimmt sie wieder weg. func TestLinkPreviewInFeed(t *testing.T) { srv := newTestServer(t) fakeRemote(t) c := registerAndLogin(t, srv, "vorschau") pid := createEntry(t, c, srv, "schau https://youtu.be/dQw4w9WgXcQ und https://example.org") key := "https://www.youtube.com/watch?v=dQw4w9WgXcQ" feedPreview := func() *Preview { t.Helper() resp, err := http.Get(srv.URL + "/api/entry/feed/0") if err != nil { t.Fatal(err) } defer resp.Body.Close() var entries []Entry if err := json.NewDecoder(resp.Body).Decode(&entries); err != nil { t.Fatal(err) } for _, e := range entries { if e.PID == pid { return e.Preview } } t.Fatalf("Beitrag %d fehlt im Feed", pid) return nil } if p := feedPreview(); p != nil { t.Errorf("Vorschau vor dem Abruf: %+v", p) } fetchPreview(context.Background(), key) p := feedPreview() if p == nil { t.Fatal("keine Vorschau nach dem Abruf") } if p.URL != key || p.Kind != "youtube" || p.Title != "Ein Video" || p.Site != "Ein Kanal" || p.Thumb != thumbPath(key) { t.Errorf("unerwartete Vorschau %+v", p) } if _, err := os.Stat(p.Thumb); err != nil { t.Errorf("thumb fehlt: %v", err) } // Ein privates/gelöschtes Video ergibt keine Vorschau. other := "https://www.youtube.com/watch?v=aaaaaaaaaaa" requestPreview(other) fetchPreview(context.Background(), other) var status string db.QueryRow(`SELECT status FROM link_preview WHERE url = $1`, other).Scan(&status) if status != "none" { t.Errorf("status = %q, erwartet none", status) } resp := postForm(t, c, srv.URL+"/api/entry/"+itoa(pid)+"/edit", url.Values{"content": {"jetzt ohne Link"}}) resp.Body.Close() if p := feedPreview(); p != nil { t.Errorf("Vorschau nach Bearbeiten ohne Link: %+v", p) } } func itoa(n int64) string { b, _ := json.Marshal(n) return string(b) }