Switch routing to chi

Routing with httprouter and alice became a bit too complex. This commit
replaces the routing and middleware composition with chi.
main
Jan Dittberner 2 years ago
parent de4c6faef6
commit 39bd724381

@ -24,8 +24,6 @@ import (
"strings"
"time"
"github.com/julienschmidt/httprouter"
"git.cacert.org/cacert-boardvoting/internal/forms"
"git.cacert.org/cacert-boardvoting/internal/models"
@ -176,9 +174,7 @@ func (app *application) calculateMotionListOptions(r *http.Request) (*models.Mot
}
func (app *application) motionDetails(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
@ -271,9 +267,7 @@ func (app *application) newMotionSubmit(w http.ResponseWriter, r *http.Request)
}
func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
@ -292,9 +286,7 @@ func (app *application) editMotionForm(w http.ResponseWriter, r *http.Request) {
}
func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
@ -371,9 +363,7 @@ func (app *application) editMotionSubmit(w http.ResponseWriter, r *http.Request)
}
func (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
app.notFound(w)
@ -388,9 +378,7 @@ func (app *application) withdrawMotionForm(w http.ResponseWriter, r *http.Reques
}
func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
app.notFound(w)
@ -426,16 +414,14 @@ func (app *application) withdrawMotionSubmit(w http.ResponseWriter, r *http.Requ
}
func (app *application) voteForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
app.notFound(w)
return
}
choice := app.choiceFromRequestParam(w, params)
choice := app.choiceFromRequestParam(w, r)
if choice == nil {
app.notFound(w)
@ -454,14 +440,12 @@ func (app *application) voteForm(w http.ResponseWriter, r *http.Request) {
}
func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
choice := app.choiceFromRequestParam(w, params)
choice := app.choiceFromRequestParam(w, r)
if choice == nil {
return
}
@ -504,9 +488,7 @@ func (app *application) voteSubmit(w http.ResponseWriter, r *http.Request) {
}
func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
@ -530,9 +512,7 @@ func (app *application) proxyVoteForm(w http.ResponseWriter, r *http.Request) {
}
func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
motion := app.motionFromRequestParam(w, r, params)
motion := app.motionFromRequestParam(w, r)
if motion == nil {
return
}
@ -635,9 +615,7 @@ func (app *application) userList(w http.ResponseWriter, r *http.Request) {
}
func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithRoles(), app.users.WithEmailAddresses())
userToEdit := app.userFromRequestParam(w, r, app.users.WithRoles(), app.users.WithEmailAddresses())
if userToEdit == nil {
return
}
@ -676,9 +654,7 @@ func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
}
func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithRoles(), app.users.WithEmailAddresses())
userToEdit := app.userFromRequestParam(w, r, app.users.WithRoles(), app.users.WithEmailAddresses())
if userToEdit == nil {
app.notFound(w)
@ -753,9 +729,7 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
}
func (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithEmailAddresses())
userToEdit := app.userFromRequestParam(w, r, app.users.WithEmailAddresses())
if userToEdit == nil {
app.notFound(w)
@ -780,9 +754,7 @@ func (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request)
}
func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context())
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithEmailAddresses())
userToEdit := app.userFromRequestParam(w, r, app.users.WithEmailAddresses())
if userToEdit == nil {
app.notFound(w)
@ -970,7 +942,7 @@ func (app *application) newUserSubmit(_ http.ResponseWriter, _ *http.Request) {
}
func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDelete())
userToDelete := app.userFromRequestParam(w, r, app.users.CanDelete())
if userToDelete == nil {
return
}
@ -991,7 +963,7 @@ func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
}
func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request) {
userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDelete())
userToDelete := app.userFromRequestParam(w, r, app.users.CanDelete())
if userToDelete == nil {
return
}

@ -32,8 +32,8 @@ import (
"strings"
"github.com/Masterminds/sprig/v3"
"github.com/go-chi/chi/v5"
"github.com/go-playground/form/v4"
"github.com/julienschmidt/httprouter"
"github.com/justinas/nosurf"
"git.cacert.org/cacert-boardvoting/internal/models"
@ -175,13 +175,10 @@ func (app *application) render(w http.ResponseWriter, status int, page string, d
func (app *application) motionFromRequestParam(
w http.ResponseWriter,
r *http.Request,
params httprouter.Params,
) *models.Motion {
tag := params.ByName("tag")
withVotes := r.URL.Query().Has("showvotes")
motion, err := app.motions.ByTag(r.Context(), tag, withVotes)
motion, err := app.motions.ByTag(r.Context(), chi.URLParam(r, "tag"), withVotes)
if err != nil {
app.serverError(w, err)
@ -198,9 +195,9 @@ func (app *application) motionFromRequestParam(
}
func (app *application) userFromRequestParam(
w http.ResponseWriter, r *http.Request, params httprouter.Params, options ...models.UserListOption,
w http.ResponseWriter, r *http.Request, options ...models.UserListOption,
) *models.User {
userID, err := strconv.Atoi(params.ByName("id"))
userID, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
app.clientError(w, http.StatusBadRequest)
@ -223,14 +220,14 @@ func (app *application) userFromRequestParam(
return user
}
func (app *application) emailFromRequestParam(w http.ResponseWriter, r *http.Request, params httprouter.Params, user *models.User) (string, error) {
emailParam := params.ByName("address")
func (app *application) emailFromRequestParam(r *http.Request, user *models.User) (string, error) {
emailAddresses, err := user.EmailAddresses()
if err != nil {
return "", fmt.Errorf("could not get email addresses: %w", err)
}
emailParam := chi.URLParam(r, "address")
for _, address := range emailAddresses {
if emailParam == address {
return emailParam, nil
@ -241,14 +238,12 @@ func (app *application) emailFromRequestParam(w http.ResponseWriter, r *http.Req
}
func (app *application) deleteEmailParams(w http.ResponseWriter, r *http.Request) (*models.User, string, error) {
params := httprouter.ParamsFromContext(r.Context())
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithEmailAddresses())
userToEdit := app.userFromRequestParam(w, r, app.users.WithEmailAddresses())
if userToEdit == nil {
return nil, "", nil
}
emailAddress, err := app.emailFromRequestParam(w, r, params, userToEdit)
emailAddress, err := app.emailFromRequestParam(r, userToEdit)
if err != nil {
return nil, "", err
}
@ -256,10 +251,8 @@ func (app *application) deleteEmailParams(w http.ResponseWriter, r *http.Request
return userToEdit, emailAddress, nil
}
func (app *application) choiceFromRequestParam(w http.ResponseWriter, params httprouter.Params) *models.VoteChoice {
choiceParam := params.ByName("choice")
choice, err := models.VoteChoiceFromString(choiceParam)
func (app *application) choiceFromRequestParam(w http.ResponseWriter, r *http.Request) *models.VoteChoice {
choice, err := models.VoteChoiceFromString(chi.URLParam(r, "choice"))
if err != nil {
app.clientError(w, http.StatusBadRequest)

@ -237,6 +237,10 @@ func (app *application) getVoter(w http.ResponseWriter, r *http.Request, voterID
return voter
}
type logEntry struct {
request *http.Request
}
func setupHTTPRedirect(config *Config, errChan chan error) {
redirect := &http.Server{
Addr: config.HTTPAddress,

@ -50,14 +50,6 @@ func secureHeaders(next http.Handler) http.Handler {
})
}
func (app *application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
})
}
func (app *application) authenticateRequest(r *http.Request) (*models.User, *x509.Certificate, error) {
if r.TLS == nil {
return nil, nil, nil
@ -198,7 +190,7 @@ func (app *application) userCanEditVote(next http.Handler) http.Handler {
return app.requireRole(next, models.RoleVoter)
}
func (app *application) userCanChangeVoters(next http.Handler) http.Handler {
func (app *application) canManageUsers(next http.Handler) http.Handler {
return app.requireRole(next, models.RoleSecretary, models.RoleAdmin)
}

@ -18,11 +18,9 @@ limitations under the License.
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
"net/http/httptest"
@ -58,35 +56,6 @@ func Test_secureHeaders(t *testing.T) {
assert.Equal(t, "max-age=63072000", rs.Header.Get("Strict-Transport-Security"))
}
func TestApplication_logRequest(t *testing.T) {
rr := httptest.NewRecorder()
r, err := http.NewRequest(http.MethodGet, "/", nil)
require.NoError(t, err)
r.RemoteAddr = "arg"
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("OK"))
})
buf := new(bytes.Buffer)
app := &application{infoLog: log.New(buf, "", log.LstdFlags)}
app.logRequest(next).ServeHTTP(rr, r)
rs := rr.Result()
assert.Equal(t, http.StatusOK, rs.StatusCode)
assert.Contains(t, buf.String(), fmt.Sprintf(
"%s - %s %s %s",
r.RemoteAddr,
r.Proto,
r.Method,
r.URL.RequestURI(),
))
}
func TestApplication_tryAuthenticate(t *testing.T) {
db := prepareTestDb(t)

@ -18,12 +18,11 @@ limitations under the License.
package main
import (
"fmt"
"io/fs"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/justinas/alice"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/vearutop/statigz"
"github.com/vearutop/statigz/brotli"
@ -40,65 +39,75 @@ func (app *application) routes() http.Handler {
fileServer := statigz.FileServer(staticData, brotli.AddEncoding, statigz.EncodeOnInit)
router := httprouter.New()
router := chi.NewRouter()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { app.notFound(w) })
router.PanicHandler = func(w http.ResponseWriter, _ *http.Request, err interface{}) {
w.Header().Set("Connection", "close")
app.serverError(w, fmt.Errorf("%s", err))
}
router.Use(middleware.RealIP)
router.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: app.infoLog}))
router.Use(middleware.Recoverer)
router.Use(secureHeaders)
router.NotFound(func(w http.ResponseWriter, _ *http.Request) { app.notFound(w) })
router.Handler(
http.MethodGet,
router.Get(
"/",
http.RedirectHandler("/motions/", http.StatusMovedPermanently),
http.RedirectHandler("/motions/", http.StatusMovedPermanently).ServeHTTP,
)
router.Handler(
http.MethodGet,
router.Get(
"/favicon.ico",
http.RedirectHandler("/static/images/favicon.ico", http.StatusMovedPermanently),
http.RedirectHandler("/static/images/favicon.ico", http.StatusMovedPermanently).ServeHTTP,
)
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
dynamic := alice.New(
app.sessionManager.LoadAndSave,
app.tryAuthenticate,
)
canVote := dynamic.Append(app.userCanVote, noSurf)
canEditVote := dynamic.Append(app.userCanEditVote, noSurf)
canManageUsers := dynamic.Append(app.userCanChangeVoters, noSurf)
router.Handler(http.MethodGet, "/motions/", dynamic.ThenFunc(app.motionList))
router.Handler(http.MethodGet, "/motions/:tag", dynamic.ThenFunc(app.motionDetails))
router.Handler(http.MethodGet, "/motions/:tag/edit", canEditVote.ThenFunc(app.editMotionForm))
router.Handler(http.MethodPost, "/motions/:tag/edit", canEditVote.ThenFunc(app.editMotionSubmit))
router.Handler(http.MethodGet, "/motions/:tag/withdraw", canEditVote.ThenFunc(app.withdrawMotionForm))
router.Handler(http.MethodPost, "/motions/:tag/withdraw", canEditVote.ThenFunc(app.withdrawMotionSubmit))
router.Handler(http.MethodGet, "/vote/:tag/:choice", canVote.ThenFunc(app.voteForm))
router.Handler(http.MethodPost, "/vote/:tag/:choice", canVote.ThenFunc(app.voteSubmit))
router.Handler(http.MethodGet, "/proxy/:tag", canVote.ThenFunc(app.proxyVoteForm))
router.Handler(http.MethodPost, "/proxy/:tag", canVote.ThenFunc(app.proxyVoteSubmit))
router.Handler(http.MethodGet, "/newmotion/", canEditVote.ThenFunc(app.newMotionForm))
router.Handler(http.MethodPost, "/newmotion/", canEditVote.ThenFunc(app.newMotionSubmit))
router.Handler(http.MethodGet, "/users/", canManageUsers.ThenFunc(app.userList))
router.Handler(http.MethodGet, "/new-user/", canManageUsers.ThenFunc(app.newUserForm))
router.Handler(http.MethodPost, "/new-user/", canManageUsers.ThenFunc(app.newUserSubmit))
router.Handler(http.MethodGet, "/users/:id/", canManageUsers.ThenFunc(app.editUserForm))
router.Handler(http.MethodPost, "/users/:id/", canManageUsers.ThenFunc(app.editUserSubmit))
router.Handler(http.MethodGet, "/users/:id/add-mail", canManageUsers.ThenFunc(app.userAddEmailForm))
router.Handler(http.MethodPost, "/users/:id/add-mail", canManageUsers.ThenFunc(app.userAddEmailSubmit))
router.Handler(http.MethodGet, "/users/:id/mail/:address/delete",
canManageUsers.ThenFunc(app.userDeleteEmailForm))
router.Handler(http.MethodPost, "/users/:id/mail/:address/delete",
canManageUsers.ThenFunc(app.userDeleteEmailSubmit))
router.Handler(http.MethodGet, "/users/:id/delete", canManageUsers.ThenFunc(app.deleteUserForm))
router.Handler(http.MethodPost, "/users/:id/delete", canManageUsers.ThenFunc(app.deleteUserSubmit))
router.HandlerFunc(http.MethodGet, "/health", app.healthCheck)
standard := alice.New(app.logRequest, secureHeaders)
return standard.Then(router)
router.Get("/static/*", http.StripPrefix("/static", fileServer).ServeHTTP)
router.Group(func(r chi.Router) {
r.Use(app.sessionManager.LoadAndSave, app.tryAuthenticate)
r.Get("/motions/", app.motionList)
r.Get("/motions/{tag}", app.motionDetails)
r.Group(func(r chi.Router) {
r.Use(app.userCanEditVote, noSurf)
r.Get("/newmotion/", app.newMotionForm)
r.Post("/newmotion/", app.newMotionSubmit)
r.Route("/motions/{tag}", func(r chi.Router) {
r.Get("/edit", app.editMotionForm)
r.Post("/edit", app.editMotionSubmit)
r.Get("/withdraw", app.withdrawMotionForm)
r.Post("/withdraw", app.withdrawMotionSubmit)
})
})
r.Group(func(r chi.Router) {
r.Use(app.userCanVote, noSurf)
r.Get("/vote/{tag}/{choice}", app.voteForm)
r.Post("/vote/{tag}/{choice}", app.voteSubmit)
r.Get("/proxy/{tag}", app.proxyVoteForm)
r.Post("/proxy/{tag}", app.proxyVoteSubmit)
})
r.Group(func(r chi.Router) {
r.Use(app.canManageUsers, noSurf)
r.Get("/users/", app.userList)
r.Get("/new-user/", app.newUserForm)
r.Post("/new-user/", app.newUserSubmit)
r.Route("/users/{id}", func(r chi.Router) {
r.Get("/", app.editUserForm)
r.Post("/", app.editUserSubmit)
r.Get("/add-mail", app.userAddEmailForm)
r.Post("/add-mail", app.userAddEmailSubmit)
r.Get("/mail/{address}/delete", app.userDeleteEmailForm)
r.Post("/mail/{address}/delete", app.userDeleteEmailSubmit)
r.Get("/delete", app.deleteUserForm)
r.Post("/delete", app.deleteUserSubmit)
})
})
})
router.Get("/health", app.healthCheck)
return router
}

@ -24,9 +24,8 @@ require (
require (
github.com/alexedwards/scs/sqlite3store v0.0.0-20220216073957-c252878bcf5a
github.com/alexedwards/scs/v2 v2.5.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-playground/form/v4 v4.2.0
github.com/julienschmidt/httprouter v1.3.0
github.com/justinas/alice v1.2.0
github.com/justinas/nosurf v1.1.1
github.com/lestrrat-go/tcputil v0.0.0-20180223003554-d3c7f98154fb
github.com/stretchr/testify v1.7.0

@ -423,6 +423,8 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
@ -751,12 +753,9 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/justinas/alice v1.2.0 h1:+MHSA/vccVCF4Uq37S42jwlkvI2Xzl7zTPCN5BnZNVo=
github.com/justinas/alice v1.2.0/go.mod h1:fN5HRH/reO/zrUflLfTN43t3vXvKzvZIENsNEe7i7qA=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=
github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=

@ -60,7 +60,7 @@ func TestDecisionModel_Create(t *testing.T) {
v := &models.User{
ID: 1, // sqlite does not check referential integrity. Might fail with a foreign key index.
Name: "test voter",
Reminder: "test+voter@example.com",
Reminder: sql.NullString{String: "test+voter@example.com", Valid: true},
}
id, err := dm.Create(
@ -93,7 +93,7 @@ func TestDecisionModel_GetNextPendingDecisionDue(t *testing.T) {
v := &models.User{
ID: 1, // sqlite does not check referential integrity. Might fail with a foreign key index.
Name: "test voter",
Reminder: "test+voter@example.com",
Reminder: sql.NullString{String: "test+voter@example.com", Valid: true},
}
due := time.Now().Add(10 * time.Minute)

Loading…
Cancel
Save