More tests and fixes

This commit is contained in:
Jan-Lukas Else 2021-06-19 08:37:16 +02:00
parent 878f3a52a9
commit f7d3138c24
22 changed files with 302 additions and 121 deletions

View File

@ -114,7 +114,7 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
} }
blogIri := a.apIri(blog) blogIri := a.apIri(blog)
// Verify request // Verify request
requestActor, requestKey, requestActorStatus, err := apVerifySignature(r) requestActor, requestKey, requestActorStatus, err := a.apVerifySignature(r)
if err != nil { if err != nil {
// Send 401 because signature could not be verified // Send 401 because signature could not be verified
a.serveError(w, r, err.Error(), http.StatusUnauthorized) a.serveError(w, r, err.Error(), http.StatusUnauthorized)
@ -217,14 +217,14 @@ func (a *goBlog) apHandleInbox(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
} }
func apVerifySignature(r *http.Request) (*asPerson, string, int, error) { func (a *goBlog) apVerifySignature(r *http.Request) (*asPerson, string, int, error) {
verifier, err := httpsig.NewVerifier(r) verifier, err := httpsig.NewVerifier(r)
if err != nil { if err != nil {
// Error with signature header etc. // Error with signature header etc.
return nil, "", 0, err return nil, "", 0, err
} }
keyID := verifier.KeyId() keyID := verifier.KeyId()
actor, statusCode, err := apGetRemoteActor(keyID) actor, statusCode, err := a.apGetRemoteActor(keyID)
if err != nil || actor == nil || statusCode != 0 { if err != nil || actor == nil || statusCode != 0 {
// Actor not found or something else bad // Actor not found or something else bad
return nil, keyID, statusCode, err return nil, keyID, statusCode, err
@ -249,14 +249,14 @@ func handleWellKnownHostMeta(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`)) _, _ = w.Write([]byte(`<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="https://` + r.Host + `/.well-known/webfinger?resource={uri}"/></XRD>`))
} }
func apGetRemoteActor(iri string) (*asPerson, int, error) { func (a *goBlog) apGetRemoteActor(iri string) (*asPerson, int, error) {
req, err := http.NewRequest(http.MethodGet, iri, nil) req, err := http.NewRequest(http.MethodGet, iri, nil)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
req.Header.Set("Accept", contenttype.AS) req.Header.Set("Accept", contenttype.AS)
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -367,7 +367,7 @@ func (a *goBlog) apAccept(blogName string, blog *configBlog, follow map[string]i
// actor and object are equal // actor and object are equal
return return
} }
follower, status, err := apGetRemoteActor(newFollower) follower, status, err := a.apGetRemoteActor(newFollower)
if err != nil || status != 0 { if err != nil || status != 0 {
// Couldn't retrieve remote actor info // Couldn't retrieve remote actor info
log.Println("Failed to retrieve remote actor info:", newFollower) log.Println("Failed to retrieve remote actor info:", newFollower)

View File

@ -114,7 +114,7 @@ func (a *goBlog) apSendSigned(blogIri, to string, activity []byte) error {
return err return err
} }
// Do request // Do request
resp, err := appHttpClient.Do(r) resp, err := a.httpClient.Do(r)
if err != nil { if err != nil {
return err return err
} }

View File

@ -17,17 +17,18 @@ const asContext = "https://www.w3.org/ns/activitystreams"
const asRequestKey requestContextKey = "asRequest" const asRequestKey requestContextKey = "asRequest"
var asCheckMediaTypes = []ct.MediaType{
ct.NewMediaType(contenttype.HTML),
ct.NewMediaType(contenttype.AS),
ct.NewMediaType(contenttype.LDJSON),
}
func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler { func (a *goBlog) checkActivityStreamsRequest(next http.Handler) http.Handler {
if len(a.asCheckMediaTypes) == 0 {
a.asCheckMediaTypes = []ct.MediaType{
ct.NewMediaType(contenttype.HTML),
ct.NewMediaType(contenttype.AS),
ct.NewMediaType(contenttype.LDJSON),
}
}
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled { if ap := a.cfg.ActivityPub; ap != nil && ap.Enabled {
// Check if accepted media type is not HTML // Check if accepted media type is not HTML
if mt, _, err := ct.GetAcceptableMediaType(r, asCheckMediaTypes); err == nil && mt.String() != asCheckMediaTypes[0].String() { if mt, _, err := ct.GetAcceptableMediaType(r, a.asCheckMediaTypes); err == nil && mt.String() != a.asCheckMediaTypes[0].String() {
next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true))) next.ServeHTTP(rw, r.WithContext(context.WithValue(r.Context(), asRequestKey, true)))
return return
} }

7
app.go
View File

@ -9,6 +9,7 @@ import (
"git.jlel.se/jlelse/GoBlog/pkgs/minify" "git.jlel.se/jlelse/GoBlog/pkgs/minify"
shutdowner "git.jlel.se/jlelse/go-shutdowner" shutdowner "git.jlel.se/jlelse/go-shutdowner"
ts "git.jlel.se/jlelse/template-strings" ts "git.jlel.se/jlelse/template-strings"
ct "github.com/elnormous/contenttype"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-fed/httpsig" "github.com/go-fed/httpsig"
rotatelogs "github.com/lestrrat-go/file-rotatelogs" rotatelogs "github.com/lestrrat-go/file-rotatelogs"
@ -23,6 +24,8 @@ type goBlog struct {
apPostSignMutex sync.Mutex apPostSignMutex sync.Mutex
webfingerResources map[string]*configBlog webfingerResources map[string]*configBlog
webfingerAccts map[string]string webfingerAccts map[string]string
// ActivityStreams
asCheckMediaTypes []ct.MediaType
// Assets // Assets
assetFileNames map[string]string assetFileNames map[string]string
assetFiles map[string]*assetFile assetFiles map[string]*assetFile
@ -36,6 +39,8 @@ type goBlog struct {
cfg *config cfg *config
// Database // Database
db *database db *database
// Errors
errorCheckMediaTypes []ct.MediaType
// Hooks // Hooks
pPostHooks []postHookFunc pPostHooks []postHookFunc
pUpdateHooks []postHookFunc pUpdateHooks []postHookFunc
@ -43,6 +48,8 @@ type goBlog struct {
hourlyHooks []hourlyHookFunc hourlyHooks []hourlyHookFunc
// HTTP // HTTP
cspDomains string cspDomains string
// HTTP Client
httpClient httpClient
// HTTP Routers // HTTP Routers
d *dynamicHandler d *dynamicHandler
privateMode bool privateMode bool

View File

@ -78,7 +78,7 @@ func (a *goBlog) getBlogrollOutlines(blog string) ([]*opml.Outline, error) {
if config.AuthHeader != "" && config.AuthValue != "" { if config.AuthHeader != "" && config.AuthValue != "" {
req.Header.Set(config.AuthHeader, config.AuthValue) req.Header.Set(config.AuthHeader, config.AuthValue)
} }
res, err := appHttpClient.Do(req) res, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -22,7 +22,6 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler {
} }
} }
// 2. Show Captcha // 2. Show Captcha
w.WriteHeader(http.StatusUnauthorized)
h, _ := json.Marshal(r.Header.Clone()) h, _ := json.Marshal(r.Header.Clone())
b, _ := io.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte b, _ := io.ReadAll(io.LimitReader(r.Body, 2000000)) // Only allow 20 Megabyte
_ = r.Body.Close() _ = r.Body.Close()
@ -31,7 +30,7 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm() _ = r.ParseForm()
b = []byte(r.PostForm.Encode()) b = []byte(r.PostForm.Encode())
} }
a.render(w, r, templateCaptcha, &renderData{ a.renderWithStatusCode(w, r, http.StatusUnauthorized, templateCaptcha, &renderData{
Data: map[string]string{ Data: map[string]string{
"captchamethod": r.Method, "captchamethod": r.Method,
"captchaheaders": base64.StdEncoding.EncodeToString(h), "captchaheaders": base64.StdEncoding.EncodeToString(h),

79
captcha_test.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/stretchr/testify/assert"
)
func Test_captchaMiddleware(t *testing.T) {
app := &goBlog{
cfg: &config{
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
"en": {
Lang: "en",
},
},
DefaultBlog: "en",
User: &configUser{},
},
}
app.setInMemoryDatabase()
app.initSessions()
_ = app.initTemplateStrings()
_ = app.initRendering()
h := app.captchaMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("ABC Test"))
}))
t.Run("Default", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/abc", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
res := rec.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusUnauthorized, res.StatusCode)
assert.Contains(t, res.Header.Get("Content-Type"), contenttype.HTML)
assert.Contains(t, resString, "name=captchamethod value=POST")
})
t.Run("Captcha session", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/abc", nil)
rec1 := httptest.NewRecorder()
session, _ := app.captchaSessions.Get(req, "c")
session.Values["captcha"] = true
session.Save(req, rec1)
for _, cookie := range rec1.Result().Cookies() {
req.AddCookie(cookie)
}
rec2 := httptest.NewRecorder()
h.ServeHTTP(rec2, req)
res := rec2.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusOK, res.StatusCode)
assert.Contains(t, resString, "ABC Test")
})
}

View File

@ -21,24 +21,24 @@ func (a *goBlog) serveNotAllowed(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, "", http.StatusMethodNotAllowed) a.serveError(w, r, "", http.StatusMethodNotAllowed)
} }
var errorCheckMediaTypes = []ct.MediaType{
ct.NewMediaType(contenttype.HTML),
}
func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message string, status int) { func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message string, status int) {
if mt, _, err := ct.GetAcceptableMediaType(r, errorCheckMediaTypes); err != nil || mt.String() != errorCheckMediaTypes[0].String() { // Init the first time
if len(a.errorCheckMediaTypes) == 0 {
a.errorCheckMediaTypes = append(a.errorCheckMediaTypes, ct.NewMediaType(contenttype.HTML))
}
// Check message
if message == "" {
message = http.StatusText(status)
}
// Check if request accepts HTML
if mt, _, err := ct.GetAcceptableMediaType(r, a.errorCheckMediaTypes); err != nil || mt.String() != a.errorCheckMediaTypes[0].String() {
// Request doesn't accept HTML // Request doesn't accept HTML
http.Error(w, message, status) http.Error(w, message, status)
return return
} }
title := fmt.Sprintf("%d %s", status, http.StatusText(status)) a.renderWithStatusCode(w, r, status, templateError, &renderData{
if message == "" {
message = http.StatusText(status)
}
w.WriteHeader(status)
a.render(w, r, templateError, &renderData{
Data: &errorData{ Data: &errorData{
Title: title, Title: fmt.Sprintf("%d %s", status, http.StatusText(status)),
Message: message, Message: message,
}, },
}) })

112
errors_test.go Normal file
View File

@ -0,0 +1,112 @@
package main
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"git.jlel.se/jlelse/GoBlog/pkgs/contenttype"
"github.com/stretchr/testify/assert"
)
func Test_errors(t *testing.T) {
app := &goBlog{
cfg: &config{
Server: &configServer{
PublicAddress: "https://example.com",
},
Blogs: map[string]*configBlog{
"en": {
Lang: "en",
},
},
DefaultBlog: "en",
User: &configUser{},
},
}
app.initMarkdown()
_ = app.initTemplateStrings()
_ = app.initRendering()
t.Run("Test 404, no HTML", func(t *testing.T) {
h := http.HandlerFunc(app.serve404)
req := httptest.NewRequest(http.MethodGet, "/abc", nil)
req.Header.Set("Accept", contenttype.JSON)
rec := httptest.NewRecorder()
h(rec, req)
res := rec.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
assert.Contains(t, resString, "not found")
assert.Contains(t, res.Header.Get("Content-Type"), "text/plain")
})
t.Run("Test 404, HTML", func(t *testing.T) {
h := http.HandlerFunc(app.serve404)
req := httptest.NewRequest(http.MethodGet, "/abc", nil)
req.Header.Set("Accept", contenttype.HTML)
rec := httptest.NewRecorder()
h(rec, req)
res := rec.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusNotFound, res.StatusCode)
assert.Contains(t, resString, "not found")
assert.Contains(t, res.Header.Get("Content-Type"), contenttype.HTML)
})
t.Run("Test Method Not Allowed, no HTML", func(t *testing.T) {
h := http.HandlerFunc(app.serveNotAllowed)
req := httptest.NewRequest(http.MethodGet, "/abc", nil)
req.Header.Set("Accept", contenttype.JSON)
rec := httptest.NewRecorder()
h(rec, req)
res := rec.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusMethodNotAllowed, res.StatusCode)
assert.Contains(t, resString, "Method Not Allowed")
assert.Contains(t, res.Header.Get("Content-Type"), "text/plain")
})
t.Run("Test Method Not Allowed", func(t *testing.T) {
h := http.HandlerFunc(app.serveNotAllowed)
req := httptest.NewRequest(http.MethodGet, "/abc", nil)
req.Header.Set("Accept", contenttype.HTML)
rec := httptest.NewRecorder()
h(rec, req)
res := rec.Result()
resBody, _ := io.ReadAll(res.Body)
_ = res.Body.Close()
resString := string(resBody)
assert.Equal(t, http.StatusMethodNotAllowed, res.StatusCode)
assert.Contains(t, resString, "Method Not Allowed")
assert.Contains(t, res.Header.Get("Content-Type"), contenttype.HTML)
})
}

12
geo.go
View File

@ -12,11 +12,11 @@ import (
"github.com/thoas/go-funk" "github.com/thoas/go-funk"
) )
func (db *database) geoTitle(g *gogeouri.Geo, lang string) string { func (a *goBlog) geoTitle(g *gogeouri.Geo, lang string) string {
if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" { if name, ok := g.Parameters["name"]; ok && len(name) > 0 && name[0] != "" {
return name[0] return name[0]
} }
ba, err := db.photonReverse(g.Latitude, g.Longitude, lang) ba, err := a.photonReverse(g.Latitude, g.Longitude, lang)
if err != nil { if err != nil {
return "" return ""
} }
@ -32,9 +32,9 @@ func (db *database) geoTitle(g *gogeouri.Geo, lang string) string {
return strings.Join(funk.FilterString([]string{name, city, state, country}, func(s string) bool { return s != "" }), ", ") return strings.Join(funk.FilterString([]string{name, city, state, country}, func(s string) bool { return s != "" }), ", ")
} }
func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error) { func (a *goBlog) photonReverse(lat, lon float64, lang string) ([]byte, error) {
cacheKey := fmt.Sprintf("photon-%v-%v-%v", lat, lon, lang) cacheKey := fmt.Sprintf("photon-%v-%v-%v", lat, lon, lang)
cache, _ := db.retrievePersistentCache(cacheKey) cache, _ := a.db.retrievePersistentCache(cacheKey)
if cache != nil { if cache != nil {
return cache, nil return cache, nil
} }
@ -51,7 +51,7 @@ func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error)
return nil, err return nil, err
} }
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -66,7 +66,7 @@ func (db *database) photonReverse(lat, lon float64, lang string) ([]byte, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_ = db.cachePersistently(cacheKey, ba) _ = a.db.cachePersistently(cacheKey, ba)
return ba, nil return ba, nil
} }

View File

@ -12,7 +12,7 @@ func (a *goBlog) healthcheck() bool {
fmt.Println(err.Error()) fmt.Println(err.Error())
return false return false
} }
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return false return false

View File

@ -9,9 +9,15 @@ type httpClient interface {
Do(req *http.Request) (*http.Response, error) Do(req *http.Request) (*http.Response, error)
} }
var appHttpClient httpClient = &http.Client{ func getHTTPClient() httpClient {
Timeout: 5 * time.Minute, return &http.Client{
Transport: &http.Transport{ Timeout: 5 * time.Minute,
DisableKeepAlives: true, Transport: &http.Transport{
}, DisableKeepAlives: true,
},
}
}
func (a *goBlog) initHTTPClient() {
a.httpClient = getHTTPClient()
} }

View File

@ -4,32 +4,15 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"sync"
) )
type fakeHttpClient struct { type fakeHttpClient struct {
req *http.Request req *http.Request
res *http.Response res *http.Response
err error err error
enabled bool
// internal
alt httpClient
mx sync.Mutex
}
var fakeAppHttpClient *fakeHttpClient
func init() {
fakeAppHttpClient = &fakeHttpClient{
alt: appHttpClient,
}
appHttpClient = fakeAppHttpClient
} }
func (c *fakeHttpClient) Do(req *http.Request) (*http.Response, error) { func (c *fakeHttpClient) Do(req *http.Request) (*http.Response, error) {
if !c.enabled {
return c.alt.Do(req)
}
c.req = req c.req = req
return c.res, c.err return c.res, c.err
} }
@ -49,14 +32,6 @@ func (c *fakeHttpClient) setFakeResponse(statusCode int, body string, err error)
} }
} }
func (c *fakeHttpClient) lock(enabled bool) { func getFakeHTTPClient() *fakeHttpClient {
c.mx.Lock() return &fakeHttpClient{}
c.clean()
c.enabled = enabled
}
func (c *fakeHttpClient) unlock() {
c.enabled = false
c.clean()
c.mx.Unlock()
} }

View File

@ -47,6 +47,7 @@ func main() {
} }
app := &goBlog{} app := &goBlog{}
app.initHTTPClient()
// Initialize config // Initialize config
if err = app.initConfig(); err != nil { if err = app.initConfig(); err != nil {

View File

@ -37,7 +37,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
} }
req.SetBasicAuth("api", config.TinifyKey) req.SetBasicAuth("api", config.TinifyKey)
req.Header.Set(contentType, contenttype.JSON) req.Header.Set(contentType, contenttype.JSON)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -64,7 +64,7 @@ func (a *goBlog) tinify(url string, config *configMicropubMedia) (location strin
} }
downloadReq.SetBasicAuth("api", config.TinifyKey) downloadReq.SetBasicAuth("api", config.TinifyKey)
downloadReq.Header.Set(contentType, contenttype.JSON) downloadReq.Header.Set(contentType, contenttype.JSON)
downloadResp, err := appHttpClient.Do(downloadReq) downloadResp, err := a.httpClient.Do(downloadReq)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -121,7 +121,7 @@ func (a *goBlog) shortPixel(url string, config *configMicropubMedia) (location s
if err != nil { if err != nil {
return "", err return "", err
} }
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -165,7 +165,7 @@ func (a *goBlog) cloudflare(url string) (location string, err error) {
if err != nil { if err != nil {
return "", err return "", err
} }
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -106,7 +106,7 @@ func (a *goBlog) serveMicropubMedia(w http.ResponseWriter, r *http.Request) {
func (a *goBlog) uploadFile(filename string, f io.Reader) (string, error) { func (a *goBlog) uploadFile(filename string, f io.Reader) (string, error) {
ms := a.cfg.Micropub.MediaStorage ms := a.cfg.Micropub.MediaStorage
if ms != nil && ms.BunnyStorageKey != "" && ms.BunnyStorageName != "" { if ms != nil && ms.BunnyStorageKey != "" && ms.BunnyStorageName != "" {
return ms.uploadToBunny(filename, f) return a.uploadToBunny(filename, f)
} }
loc, err := saveMediaFile(filename, f) loc, err := saveMediaFile(filename, f)
if err != nil { if err != nil {
@ -118,13 +118,14 @@ func (a *goBlog) uploadFile(filename string, f io.Reader) (string, error) {
return a.getFullAddress(loc), nil return a.getFullAddress(loc), nil
} }
func (config *configMicropubMedia) uploadToBunny(filename string, f io.Reader) (location string, err error) { func (a *goBlog) uploadToBunny(filename string, f io.Reader) (location string, err error) {
config := a.cfg.Micropub.MediaStorage
if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" { if config == nil || config.BunnyStorageName == "" || config.BunnyStorageKey == "" || config.MediaURL == "" {
return "", errors.New("Bunny storage not completely configured") return "", errors.New("Bunny storage not completely configured")
} }
req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("https://storage.bunnycdn.com/%s/%s", url.PathEscape(config.BunnyStorageName), url.PathEscape(filename)), f) req, _ := http.NewRequest(http.MethodPut, fmt.Sprintf("https://storage.bunnycdn.com/%s/%s", url.PathEscape(config.BunnyStorageName), url.PathEscape(filename)), f)
req.Header.Add("AccessKey", config.BunnyStorageKey) req.Header.Add("AccessKey", config.BunnyStorageKey)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -30,7 +30,7 @@ func (a *goBlog) sendNotification(text string) {
log.Println("Failed to save notification:", err.Error()) log.Println("Failed to save notification:", err.Error())
} }
if an := a.cfg.Notifications; an != nil { if an := a.cfg.Notifications; an != nil {
err := an.Telegram.send(n.Text, "") err := a.send(an.Telegram, n.Text, "")
if err != nil { if err != nil {
log.Println("Failed to send Telegram notification:", err.Error()) log.Println("Failed to send Telegram notification:", err.Error())
} }

View File

@ -65,7 +65,7 @@ func (a *goBlog) initRendering() error {
"sort": sortedStrings, "sort": sortedStrings,
"absolute": a.getFullAddress, "absolute": a.getFullAddress,
"mentions": a.db.getWebmentionsByAddress, "mentions": a.db.getWebmentionsByAddress,
"geotitle": a.db.geoTitle, "geotitle": a.geoTitle,
"geolink": geoOSMLink, "geolink": geoOSMLink,
"opensearch": openSearchUrl, "opensearch": openSearchUrl,
} }
@ -106,6 +106,10 @@ type renderData struct {
} }
func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, data *renderData) { func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string, data *renderData) {
a.renderWithStatusCode(w, r, http.StatusOK, template, data)
}
func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, template string, data *renderData) {
// Server timing // Server timing
t := servertiming.FromContext(r.Context()).NewMetric("r").Start() t := servertiming.FromContext(r.Context()).NewMetric("r").Start()
// Check render data // Check render data
@ -153,6 +157,7 @@ func (a *goBlog) render(w http.ResponseWriter, r *http.Request, template string,
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
w.WriteHeader(statusCode)
_, err = a.min.Write(w, contenttype.HTML, tw.Bytes()) _, err = a.min.Write(w, contenttype.HTML, tw.Bytes())
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)

View File

@ -17,7 +17,7 @@ func (a *goBlog) initTelegram() {
a.pPostHooks = append(a.pPostHooks, func(p *post) { a.pPostHooks = append(a.pPostHooks, func(p *post) {
if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() { if tg := a.cfg.Blogs[p.Blog].Telegram; tg.enabled() && p.isPublishedSectionPost() {
if html := tg.generateHTML(p.Title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" { if html := tg.generateHTML(p.Title(), a.fullPostURL(p), a.shortPostURL(p)); html != "" {
if err := tg.send(html, "HTML"); err != nil { if err := a.send(tg, html, "HTML"); err != nil {
log.Printf("Failed to send post to Telegram: %v", err) log.Printf("Failed to send post to Telegram: %v", err)
} }
} }
@ -54,7 +54,7 @@ func (tg *configTelegram) generateHTML(title, fullURL, shortURL string) string {
return message.String() return message.String()
} }
func (tg *configTelegram) send(message, mode string) error { func (a *goBlog) send(tg *configTelegram, message, mode string) error {
if !tg.enabled() { if !tg.enabled() {
return nil return nil
} }
@ -70,7 +70,7 @@ func (tg *configTelegram) send(message, mode string) error {
} }
tgURL.RawQuery = params.Encode() tgURL.RawQuery = params.Encode()
req, _ := http.NewRequest(http.MethodPost, tgURL.String(), nil) req, _ := http.NewRequest(http.MethodPost, tgURL.String(), nil)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }

View File

@ -4,6 +4,8 @@ import (
"net/http" "net/http"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
func Test_configTelegram_enabled(t *testing.T) { func Test_configTelegram_enabled(t *testing.T) {
@ -69,8 +71,7 @@ func Test_configTelegram_generateHTML(t *testing.T) {
} }
func Test_configTelegram_send(t *testing.T) { func Test_configTelegram_send(t *testing.T) {
fakeAppHttpClient.lock(true) fakeClient := getFakeHTTPClient()
defer fakeAppHttpClient.unlock()
tg := &configTelegram{ tg := &configTelegram{
Enabled: true, Enabled: true,
@ -78,25 +79,19 @@ func Test_configTelegram_send(t *testing.T) {
BotToken: "bottoken", BotToken: "bottoken",
} }
fakeAppHttpClient.setFakeResponse(200, "", nil) app := &goBlog{
httpClient: fakeClient,
err := tg.send("Message", "HTML")
if err != nil {
t.Fatalf("Error: %v", err)
} }
if fakeAppHttpClient.req == nil { fakeClient.setFakeResponse(200, "", nil)
t.Error("Empty request")
} err := app.send(tg, "Message", "HTML")
if fakeAppHttpClient.err != nil { assert.Nil(t, err)
t.Error("Error in request")
} assert.NotNil(t, fakeClient.req)
if fakeAppHttpClient.req.Method != http.MethodPost { assert.Nil(t, fakeClient.err)
t.Error("Wrong method") assert.Equal(t, http.MethodPost, fakeClient.req.Method)
} assert.Equal(t, "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Message", fakeClient.req.URL.String())
if u := fakeAppHttpClient.req.URL.String(); u != "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Message" {
t.Errorf("Wrong request URL, got: %v", u)
}
} }
func Test_goBlog_initTelegram(t *testing.T) { func Test_goBlog_initTelegram(t *testing.T) {
@ -113,10 +108,9 @@ func Test_goBlog_initTelegram(t *testing.T) {
func Test_telegram(t *testing.T) { func Test_telegram(t *testing.T) {
t.Run("Send post to Telegram", func(t *testing.T) { t.Run("Send post to Telegram", func(t *testing.T) {
fakeAppHttpClient.lock(true) fakeClient := getFakeHTTPClient()
defer fakeAppHttpClient.unlock()
fakeAppHttpClient.setFakeResponse(200, "", nil) fakeClient.setFakeResponse(200, "", nil)
app := &goBlog{ app := &goBlog{
pPostHooks: []postHookFunc{}, pPostHooks: []postHookFunc{},
@ -134,6 +128,7 @@ func Test_telegram(t *testing.T) {
}, },
}, },
}, },
httpClient: fakeClient,
} }
app.setInMemoryDatabase() app.setInMemoryDatabase()
@ -152,16 +147,17 @@ func Test_telegram(t *testing.T) {
app.pPostHooks[0](p) app.pPostHooks[0](p)
if u := fakeAppHttpClient.req.URL.String(); u != "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Title%0A%0A%3Ca+href%3D%22https%3A%2F%2Fexample.com%2Fs%2F1%22%3Ehttps%3A%2F%2Fexample.com%2Fs%2F1%3C%2Fa%3E" { assert.Equal(
t.Errorf("Wrong request URL, got: %v", u) t,
} "https://api.telegram.org/botbottoken/sendMessage?chat_id=chatid&parse_mode=HTML&text=Title%0A%0A%3Ca+href%3D%22https%3A%2F%2Fexample.com%2Fs%2F1%22%3Ehttps%3A%2F%2Fexample.com%2Fs%2F1%3C%2Fa%3E",
fakeClient.req.URL.String(),
)
}) })
t.Run("Telegram disabled", func(t *testing.T) { t.Run("Telegram disabled", func(t *testing.T) {
fakeAppHttpClient.lock(true) fakeClient := getFakeHTTPClient()
defer fakeAppHttpClient.unlock()
fakeAppHttpClient.setFakeResponse(200, "", nil) fakeClient.setFakeResponse(200, "", nil)
app := &goBlog{ app := &goBlog{
pPostHooks: []postHookFunc{}, pPostHooks: []postHookFunc{},
@ -173,6 +169,7 @@ func Test_telegram(t *testing.T) {
"en": {}, "en": {},
}, },
}, },
httpClient: fakeClient,
} }
app.setInMemoryDatabase() app.setInMemoryDatabase()
@ -191,8 +188,6 @@ func Test_telegram(t *testing.T) {
app.pPostHooks[0](p) app.pPostHooks[0](p)
if fakeAppHttpClient.req != nil { assert.Nil(t, fakeClient.req)
t.Error("There should be no request")
}
}) })
} }

View File

@ -47,11 +47,11 @@ func (a *goBlog) sendWebmentions(p *post) error {
// Just ignore the mention // Just ignore the mention
continue continue
} }
endpoint := discoverEndpoint(link) endpoint := a.discoverEndpoint(link)
if endpoint == "" { if endpoint == "" {
continue continue
} }
if err = sendWebmention(endpoint, a.fullPostURL(p), link); err != nil { if err = a.sendWebmention(endpoint, a.fullPostURL(p), link); err != nil {
log.Println("Sending webmention to " + link + " failed") log.Println("Sending webmention to " + link + " failed")
continue continue
} }
@ -60,7 +60,7 @@ func (a *goBlog) sendWebmentions(p *post) error {
return nil return nil
} }
func sendWebmention(endpoint, source, target string) error { func (a *goBlog) sendWebmention(endpoint, source, target string) error {
req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(url.Values{ req, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(url.Values{
"source": []string{source}, "source": []string{source},
"target": []string{target}, "target": []string{target},
@ -70,7 +70,7 @@ func sendWebmention(endpoint, source, target string) error {
} }
req.Header.Set(contentType, contenttype.WWWForm) req.Header.Set(contentType, contenttype.WWWForm)
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
res, err := appHttpClient.Do(req) res, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }
@ -82,14 +82,14 @@ func sendWebmention(endpoint, source, target string) error {
return nil return nil
} }
func discoverEndpoint(urlStr string) string { func (a *goBlog) discoverEndpoint(urlStr string) string {
doRequest := func(method, urlStr string) string { doRequest := func(method, urlStr string) string {
req, err := http.NewRequest(method, urlStr, nil) req, err := http.NewRequest(method, urlStr, nil)
if err != nil { if err != nil {
return "" return ""
} }
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
resp, err := appHttpClient.Do(req) resp, err := a.httpClient.Do(req)
if err != nil { if err != nil {
return "" return ""
} }

View File

@ -78,7 +78,7 @@ func (a *goBlog) verifyMention(m *mention) error {
resp = rec.Result() resp = rec.Result()
} else { } else {
req.Header.Set(userAgent, appUserAgent) req.Header.Set(userAgent, appUserAgent)
resp, err = appHttpClient.Do(req) resp, err = a.httpClient.Do(req)
if err != nil { if err != nil {
return err return err
} }