Make web tests database-free and finish review hardening.
Introduce a Store interface with Postgres and in-memory backends, cover mutations/CSRF/session rotation without Postgres, bound avatar decode dimensions, add truncate/prepareAvatar unit tests, and run go test -race in CI.
This commit is contained in:
+26
-25
@@ -3,7 +3,6 @@ package web
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"html/template"
|
||||
@@ -31,7 +30,7 @@ type Config struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
db *sql.DB
|
||||
store store.Store
|
||||
sessions *scs.SessionManager
|
||||
tmpl *template.Template
|
||||
cfg Config
|
||||
@@ -85,7 +84,7 @@ type voteCtx struct {
|
||||
Question store.RankedQuestion
|
||||
}
|
||||
|
||||
func New(db *sql.DB, sessionStore scs.Store, templateFS fs.FS, staticFS fs.FS, cfg Config) (*Server, error) {
|
||||
func New(st store.Store, sessionStore scs.Store, templateFS fs.FS, staticFS fs.FS, cfg Config) (*Server, error) {
|
||||
if cfg.Blob == nil {
|
||||
cfg.Blob = blob.Disabled{}
|
||||
}
|
||||
@@ -125,7 +124,7 @@ func New(db *sql.DB, sessionStore scs.Store, templateFS fs.FS, staticFS fs.FS, c
|
||||
}
|
||||
|
||||
return &Server{
|
||||
db: db,
|
||||
store: st,
|
||||
sessions: sessions,
|
||||
tmpl: tmpl,
|
||||
cfg: cfg,
|
||||
@@ -181,7 +180,7 @@ func (s *Server) withUser(next http.Handler) http.Handler {
|
||||
}
|
||||
id := s.sessions.GetString(r.Context(), "user_id")
|
||||
if id != "" {
|
||||
u, err := store.UserByID(r.Context(), s.db, id)
|
||||
u, err := s.store.UserByID(r.Context(), id)
|
||||
if err == nil {
|
||||
r = r.WithContext(context.WithValue(r.Context(), userKey, u))
|
||||
}
|
||||
@@ -259,7 +258,7 @@ func (s *Server) renderHunt(w http.ResponseWriter, r *http.Request, date string)
|
||||
if u := currentUser(r); u != nil {
|
||||
viewer = u.ID
|
||||
}
|
||||
questions, err := store.ListHunt(r.Context(), s.db, date, viewer)
|
||||
questions, err := s.store.ListHunt(r.Context(), date, viewer)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load questions", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -319,12 +318,13 @@ func (s *Server) handleSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
if len(city) > 80 {
|
||||
city = truncateRunes(city, 80)
|
||||
}
|
||||
q := store.NewQuestion(s.db)
|
||||
q.AuthorID = u.ID
|
||||
q.Title = title
|
||||
q.Body = body
|
||||
q.City = city
|
||||
if err := q.Create(r.Context()); err != nil {
|
||||
q := &store.RankedQuestion{
|
||||
AuthorID: u.ID,
|
||||
Title: title,
|
||||
Body: body,
|
||||
City: city,
|
||||
}
|
||||
if err := s.store.CreateQuestion(r.Context(), q); err != nil {
|
||||
http.Error(w, "could not save question", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -337,14 +337,14 @@ func (s *Server) handleQuestion(w http.ResponseWriter, r *http.Request) {
|
||||
if u := currentUser(r); u != nil {
|
||||
viewer = u.ID
|
||||
}
|
||||
q, err := store.GetQuestion(r.Context(), s.db, id, viewer)
|
||||
q, err := s.store.GetQuestion(r.Context(), id, viewer)
|
||||
if err != nil || (q.Hidden && !currentUser(r).Admin()) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
var ans *store.Answer
|
||||
if q.Answered {
|
||||
ans, _ = store.GetAnswer(r.Context(), s.db, q.ID)
|
||||
ans, _ = s.store.GetAnswer(r.Context(), q.ID)
|
||||
}
|
||||
s.exec(w, "question", questionPage{
|
||||
page: s.basePage(r, q.Title),
|
||||
@@ -377,7 +377,7 @@ func (s *Server) handleVote(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "invalid vote", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := store.Vote(r.Context(), s.db, u.ID, id, value); err != nil {
|
||||
if err := s.store.Vote(r.Context(), u.ID, id, value); err != nil {
|
||||
http.Error(w, "could not vote", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@@ -388,7 +388,7 @@ func (s *Server) handleVote(w http.ResponseWriter, r *http.Request) {
|
||||
s.renderLeaderboard(w, r, date)
|
||||
return
|
||||
}
|
||||
q, err := store.GetQuestion(r.Context(), s.db, id, u.ID)
|
||||
q, err := s.store.GetQuestion(r.Context(), id, u.ID)
|
||||
if err != nil {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
@@ -421,7 +421,7 @@ func (s *Server) renderLeaderboard(w http.ResponseWriter, r *http.Request, date
|
||||
if u := currentUser(r); u != nil {
|
||||
viewer = u.ID
|
||||
}
|
||||
questions, err := store.ListHunt(r.Context(), s.db, date, viewer)
|
||||
questions, err := s.store.ListHunt(r.Context(), date, viewer)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load questions", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -451,15 +451,16 @@ func (s *Server) handleAnswer(w http.ResponseWriter, r *http.Request) {
|
||||
if len(body) > 12000 {
|
||||
body = truncateRunes(body, 12000)
|
||||
}
|
||||
ans := store.NewAnswer(s.db)
|
||||
ans.QuestionID = id
|
||||
ans.AuthorID = u.ID
|
||||
ans.Body = body
|
||||
if err := ans.Upsert(r.Context()); err != nil {
|
||||
ans := &store.Answer{
|
||||
QuestionID: id,
|
||||
AuthorID: u.ID,
|
||||
Body: body,
|
||||
}
|
||||
if err := s.store.UpsertAnswer(r.Context(), ans); err != nil {
|
||||
http.Error(w, "could not save answer", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
saved, err := store.GetAnswer(r.Context(), s.db, id)
|
||||
saved, err := s.store.GetAnswer(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "could not load answer", http.StatusInternalServerError)
|
||||
return
|
||||
@@ -481,12 +482,12 @@ func (s *Server) handleHide(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
id := chi.URLParam(r, "id")
|
||||
q, err := store.GetQuestion(r.Context(), s.db, id, u.ID)
|
||||
q, err := s.store.GetQuestion(r.Context(), id, u.ID)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := q.Hide(r.Context()); err != nil {
|
||||
if err := s.store.HideQuestion(r.Context(), id); err != nil {
|
||||
http.Error(w, "could not hide", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user