resolve dangliing hashes

This commit is contained in:
2026-04-21 11:47:56 +02:00
parent 682b7a0228
commit 0a25bd559c
14 changed files with 390 additions and 59 deletions
-5
View File
@@ -10,11 +10,6 @@ func notFound(slug string, err error, w http.ResponseWriter, r *http.Request) {
respondWithError(slug, err, w, r, http.StatusNotFound)
}
// badRequest handles 400 Bad Request errors.
func badRequest(slug string, err error, w http.ResponseWriter, r *http.Request) {
respondWithError(slug, err, w, r, http.StatusBadRequest)
}
// internal handles 500 Internal Server Error errors.
func internal(slug string, err error, w http.ResponseWriter, r *http.Request) {
respondWithError(slug, err, w, r, http.StatusInternalServerError)
+1 -1
View File
@@ -87,7 +87,7 @@ func (h *HttpHandler) HandleSet(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.store.Set(id, contentHash, content); err != nil {
if err := h.store.Set(id, contentHash, content, nil); err != nil {
internal("could not save bin", err, w, r)
return
}
+159 -4
View File
@@ -1,7 +1,9 @@
package handler
import (
"crypto/rand"
"errors"
"html/template"
"net/http"
"net/http/httptest"
"net/url"
@@ -31,8 +33,8 @@ func (m *MockStore) GetIDByHash(hash string) (string, bool, error) {
return args.String(0), args.Bool(1), args.Error(2)
}
func (m *MockStore) Set(id, hash, content string) error {
args := m.Called(id, hash, content)
func (m *MockStore) Set(id, hash, content string, metadata map[string]interface{}) error {
args := m.Called(id, hash, content, metadata)
return args.Error(0)
}
@@ -57,7 +59,7 @@ func TestHandleSet(t *testing.T) {
content := "new content"
ch := hash(content)
s.On("GetIDByHash", ch).Return("", false, nil).Once()
s.On("Set", mock.Anything, ch, content).Return(nil).Once()
s.On("Set", mock.Anything, ch, content, mock.Anything).Return(nil).Once()
form := url.Values{"content": {content}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
@@ -120,7 +122,7 @@ func TestHandleSet(t *testing.T) {
content := "error content"
ch := hash(content)
s.On("GetIDByHash", ch).Return("", false, nil).Once()
s.On("Set", mock.Anything, ch, content).Return(errors.New("db error")).Once()
s.On("Set", mock.Anything, ch, content, mock.Anything).Return(errors.New("db error")).Once()
form := url.Values{"content": {content}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
@@ -130,6 +132,44 @@ func TestHandleSet(t *testing.T) {
h.HandleSet(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
t.Run("Generate ID Error", func(t *testing.T) {
originalReader := rand.Reader
defer func() { rand.Reader = originalReader }()
rand.Reader = errorReader{}
content := "generate error content"
ch := hash(content)
s.On("GetIDByHash", ch).Return("", false, nil).Once()
form := url.Values{"content": {content}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
h.HandleSet(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
t.Run("Malformed Form Data", func(t *testing.T) {
req := httptest.NewRequest("POST", "/", strings.NewReader("content=%zz"))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
h.HandleSet(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid form data")
})
t.Run("Too Large Content", func(t *testing.T) {
content := strings.Repeat("a", 2048)
form := url.Values{"content": {content}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
h.HandleSet(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Content too large")
})
}
func TestHandleGet(t *testing.T) {
@@ -167,6 +207,18 @@ func TestHandleGet(t *testing.T) {
h.HandleGet(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
t.Run("With Extension", func(t *testing.T) {
id := "testid"
s.On("Get", id).Return(&store.Paste{Content: "hello", CreatedAt: time.Now()}, true, nil).Once()
req := httptest.NewRequest("GET", "/"+id+".go", nil)
req.SetPathValue("id", id+".go")
rr := httptest.NewRecorder()
h.HandleGet(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "hello")
})
}
type FailingResponseWriter struct {
@@ -184,6 +236,14 @@ func TestHandleHomeError(t *testing.T) {
h.HandleHome(rr, req)
}
type mockTemplateStore struct {
mock.Mock
}
func (m *mockTemplateStore) ExecuteTemplate(w http.ResponseWriter, name string, data interface{}) error {
return errors.New("template error")
}
func TestHandleRaw(t *testing.T) {
s := new(MockStore)
h := NewHandler(s, 1024, "../view/templates/*.html")
@@ -202,6 +262,19 @@ func TestHandleRaw(t *testing.T) {
assert.Equal(t, "text/plain; charset=utf-8", rr.Header().Get("Content-Type"))
})
t.Run("With Extension", func(t *testing.T) {
id := "testid"
content := "raw content"
s.On("Get", id).Return(&store.Paste{Content: content}, true, nil).Once()
req := httptest.NewRequest("GET", "/raw/"+id+".txt", nil)
req.SetPathValue("id", id+".txt")
rr := httptest.NewRecorder()
h.HandleRaw(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, content, rr.Body.String())
})
t.Run("Not Found", func(t *testing.T) {
s.On("Get", "missing").Return(nil, false, nil).Once()
req := httptest.NewRequest("GET", "/raw/missing", nil)
@@ -211,6 +284,16 @@ func TestHandleRaw(t *testing.T) {
h.HandleRaw(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
})
t.Run("Store Error", func(t *testing.T) {
s.On("Get", "error").Return(nil, false, errors.New("db error")).Once()
req := httptest.NewRequest("GET", "/raw/error", nil)
req.SetPathValue("id", "error")
rr := httptest.NewRecorder()
h.HandleRaw(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
}
func TestHandleGetTemplateError(t *testing.T) {
@@ -224,3 +307,75 @@ func TestHandleGetTemplateError(t *testing.T) {
h.HandleGet(rr, req)
}
func TestHandleSetTemplateError(t *testing.T) {
h := NewHandler(nil, 1024, "../view/templates/*.html")
t.Run("Empty Content", func(t *testing.T) {
form := url.Values{"content": {""}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := &FailingResponseWriter{*httptest.NewRecorder()}
h.HandleSet(rr, req)
})
t.Run("Parse Error", func(t *testing.T) {
req := httptest.NewRequest("POST", "/", strings.NewReader("content=%zz"))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := &FailingResponseWriter{*httptest.NewRecorder()}
h.HandleSet(rr, req)
})
}
func TestTemplateErrors(t *testing.T) {
tmpl := template.New("empty")
h := &HttpHandler{
templates: tmpl,
maxSize: 1024,
}
t.Run("HandleHome Error", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
h.HandleHome(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
t.Run("HandleGet Error", func(t *testing.T) {
s := new(MockStore)
h.store = s
id := "testid"
s.On("Get", id).Return(&store.Paste{Content: "hello", CreatedAt: time.Now()}, true, nil).Once()
req := httptest.NewRequest("GET", "/"+id, nil)
req.SetPathValue("id", id)
rr := httptest.NewRecorder()
h.HandleGet(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
})
t.Run("HandleSet Empty Content Error", func(t *testing.T) {
form := url.Values{"content": {""}}
req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
h.HandleSet(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
t.Run("HandleSet Parse Error", func(t *testing.T) {
req := httptest.NewRequest("POST", "/", strings.NewReader("content=%zz"))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
h.HandleSet(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
}
func TestSafeHTML(t *testing.T) {
h := NewHandler(nil, 1024, "../view/templates/*.html")
tmpl, err := h.templates.New("test").Parse(`{{ safeHTML "<br>" }}`)
assert.NoError(t, err)
rr := httptest.NewRecorder()
err = tmpl.Execute(rr, nil)
assert.NoError(t, err)
assert.Equal(t, "<br>", rr.Body.String())
}
+10 -9
View File
@@ -5,22 +5,23 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math/big"
"time"
)
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
func generateId() (string, error) {
bytes := make([]byte, 10)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return "", err
result := make([]byte, 10)
max := big.NewInt(int64(len(charset)))
for i := 0; i < 10; i++ {
n, err := rand.Int(rand.Reader, max)
if err != nil {
return "", err
}
result[i] = charset[n.Int64()]
}
for i := range bytes {
bytes[i] = charset[bytes[i]%byte(len(charset))]
}
return string(bytes), nil
return string(result), nil
}
func TimeAgo(t time.Time) string {
+18
View File
@@ -1,18 +1,36 @@
package handler
import (
"crypto/rand"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type errorReader struct{}
func (e errorReader) Read(p []byte) (n int, err error) {
return 0, errors.New("read error")
}
func TestGenerateId(t *testing.T) {
id, err := generateId()
assert.NoError(t, err)
assert.Equal(t, 10, len(id))
}
func TestGenerateIdError(t *testing.T) {
originalReader := rand.Reader
defer func() { rand.Reader = originalReader }()
rand.Reader = errorReader{}
id, err := generateId()
assert.Error(t, err)
assert.Empty(t, id)
}
func TestHash(t *testing.T) {
c := "test content"
h1 := hash(c)