package adapter import ( "bytes" "context" "io" "mime" "mime/multipart" "net/http" "net/http/httptest" "net/url" "strings" "testing" ) var pngBytes = []byte("\x89PNG\r\n\x1a\nfake-image-bytes") // imageServer serves one image over TLS and records the Authorization header. func imageServer(t *testing.T, contentType string, body []byte) (*httptest.Server, *string) { t.Helper() var auth string srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth = r.Header.Get("Authorization") w.Header().Set("Content-Type", contentType) _, _ = w.Write(body) })) t.Cleanup(srv.Close) return srv, &auth } func fetcherFor(srv *httptest.Server, token string) *VisualFetcher { u, _ := url.Parse(srv.URL) f := NewVisualFetcher([]string{u.Hostname()}, token) client := srv.Client() client.CheckRedirect = f.hc.CheckRedirect f.hc = client return f } func visualReq(t *testing.T, visualURL string) postRequest { t.Helper() req := validReq("p-visual") req.VisualURL = visualURL return req } func TestPublishUploadsFetchedImage(t *testing.T) { srv, auth := imageServer(t, "image/png", pngBytes) snd := &fakeSender{} a := New(Config{ChatID: 1, AttachVisual: true, Visuals: fetcherFor(srv, "read-token-123")}, snd, NewMemStore(), nil) w := post(t, a.Handler(), visualReq(t, srv.URL+"/coulomb/x/raw/branch/main/assets/a.png")) if w.Code != http.StatusCreated { t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) } if len(snd.uploads) != 1 || !bytes.Equal(snd.uploads[0].Data, pngBytes) { t.Fatalf("expected the fetched bytes uploaded, got %d uploads", len(snd.uploads)) } if snd.uploads[0].Filename != "a.png" || snd.uploads[0].ContentType != "image/png" { t.Fatalf("upload = %+v", snd.uploads[0]) } if *auth != "token read-token-123" { t.Fatalf("authorization header = %q", *auth) } } func TestPublishRefusesUnusableVisuals(t *testing.T) { big := append([]byte{}, pngBytes...) big = append(big, bytes.Repeat([]byte{0}, MaxPhotoBytes)...) cases := []struct { name, ct string body []byte other bool }{ {"host not allowlisted", "image/png", pngBytes, true}, {"not an image", "text/html", []byte(""), false}, {"over the photo limit", "image/png", big, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { srv, _ := imageServer(t, tc.ct, tc.body) f := fetcherFor(srv, "read-token-123") if tc.other { f = NewVisualFetcher([]string{"forgejo.example"}, "read-token-123") } snd := &fakeSender{} a := New(Config{ChatID: 1, AttachVisual: true, Visuals: f}, snd, NewMemStore(), nil) w := post(t, a.Handler(), visualReq(t, srv.URL+"/a.png")) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, body = %s", w.Code, w.Body.String()) } if len(snd.uploads)+len(snd.sent)+len(snd.captions) != 0 { t.Fatal("something was sent to Telegram") } if strings.Contains(w.Body.String(), "read-token-123") { t.Fatal("the read token appeared in the response") } }) } } func TestFetchRefusesPlainHTTP(t *testing.T) { f := NewVisualFetcher([]string{"forgejo.coulomb.social"}, "") if _, err := f.Fetch(context.Background(), "http://forgejo.coulomb.social/a.png"); err == nil { t.Fatal("fetched over plain http") } } type captureTransport struct { req *http.Request body []byte } func (c *captureTransport) RoundTrip(r *http.Request) (*http.Response, error) { c.req = r c.body, _ = io.ReadAll(r.Body) return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"ok":true,"result":{"message_id":9}}`))}, nil } func TestBotAPIUploadPhotoIsMultipart(t *testing.T) { ct := &captureTransport{} b := NewBotAPISender("123:secret-bot-token") b.hc = &http.Client{Transport: ct} id, err := b.UploadPhoto(context.Background(), -1004427430639, Photo{Data: pngBytes, Filename: "a.png", ContentType: "image/png"}, "caption x") if err != nil || id != 9 { t.Fatalf("id=%d err=%v", id, err) } if ct.req.URL.Path != "/bot123:secret-bot-token/sendPhoto" { t.Fatalf("path = %s", ct.req.URL.Path) } _, params, err := mime.ParseMediaType(ct.req.Header.Get("Content-Type")) if err != nil { t.Fatal(err) } mr := multipart.NewReader(bytes.NewReader(ct.body), params["boundary"]) got := map[string][]byte{} for { p, err := mr.NextPart() if err != nil { break } data, _ := io.ReadAll(p) got[p.FormName()] = data } if string(got["chat_id"]) != "-1004427430639" || string(got["parse_mode"]) != "HTML" || string(got["caption"]) != "caption x" || !bytes.Equal(got["photo"], pngBytes) { t.Fatalf("multipart fields = %v", got) } } type failTransport struct{} func (failTransport) RoundTrip(*http.Request) (*http.Response, error) { return nil, io.ErrUnexpectedEOF } func TestBotAPIUploadErrorHidesToken(t *testing.T) { b := NewBotAPISender("123:secret-bot-token") b.hc = &http.Client{Transport: failTransport{}} _, err := b.UploadPhoto(context.Background(), 1, Photo{Data: pngBytes, Filename: "a.png", ContentType: "image/png"}, "c") if err == nil || strings.Contains(err.Error(), "secret-bot-token") { t.Fatalf("err = %v", err) } }