Testing with mocks
continuous-integration/drone/push Build is passing Details

main
Drew Bednar 2 months ago
parent e9232a0838
commit bad8cf3009

@ -16,6 +16,7 @@ import (
rdb "git.runcible.io/learning/ratchet/internal/database"
"git.runcible.io/learning/ratchet/internal/logging"
"git.runcible.io/learning/ratchet/internal/model"
"git.runcible.io/learning/ratchet/internal/server"
"github.com/alexedwards/scs/sqlite3store"
"github.com/alexedwards/scs/v2"
@ -76,7 +77,7 @@ func run(ctx context.Context, w io.Writer, args []string) error {
// for a GET request, so the user won't be treated as being "logged in" to the app even if they
// did in another tab
// sm.Cookie.SameSite = http.SameSiteStrictMode
app := server.NewRatchetApp(logger, tc, db, sm)
app := server.NewRatchetApp(logger, tc, &model.SnippetService{DB: db}, &model.UserService{DB: db}, sm)
// these two elliptic curves have assembly implementations
tlsConfig := tls.Config{

@ -56,3 +56,58 @@ func TestPingIntegration(t *testing.T) {
assert.Equal(t, code, http.StatusOK)
assert.Equal(t, body, "OK")
}
func TestSnippetView(t *testing.T) {
t.Parallel()
app := newTestApplication(t)
ts := newTestServer(t, app.Routes())
defer ts.Close()
tests := []struct {
name string
urlPath string
wantCode int
wantBody string
}{
{
name: "Valid ID",
urlPath: "/snippet/view/1",
wantCode: 200,
wantBody: "Hello golang mocking",
},
{
name: "Nonexistent ID",
urlPath: "/snippet/view/2",
wantCode: 404,
},
{
name: "Negative ID",
urlPath: "/snippet/view/-1",
wantCode: 404,
},
{
name: "Decimal ID",
urlPath: "/snippet/view/1.23",
wantCode: 404,
},
{
name: "string ID",
urlPath: "/snippet/view/foo",
wantCode: 404,
},
{
name: "emptry ID",
urlPath: "/snippet/view/",
wantCode: 404,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
code, _, body := ts.get(t, test.urlPath)
assert.Equal(t, code, test.wantCode)
assert.StringContains(t, body, test.wantBody)
})
}
}

@ -7,14 +7,28 @@ import (
"net/http/cookiejar"
"net/http/httptest"
"testing"
"time"
"git.runcible.io/learning/ratchet/internal/model/mock"
"git.runcible.io/learning/ratchet/internal/server"
"github.com/alexedwards/scs/v2"
)
// Create a newTestApplication helper which returns an instance of our
// application struct containing mocked dependencies.
func newTestApplication(t *testing.T) *server.RatchetApp {
rs := server.NewRatchetApp(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, nil)
//tc, err := server.InitTemplateCache()
tc, err := server.InitFSTemplateCache()
if err != nil {
t.Fatal(err)
}
sessionManager := scs.New()
sessionManager.Lifetime = 12 * time.Hour
sessionManager.Cookie.Secure = true
rs := server.NewRatchetApp(slog.New(slog.NewTextHandler(io.Discard, nil)), tc, &mock.SnippetService{}, &mock.UserService{}, sessionManager)
return rs
}

@ -1,6 +1,9 @@
package assert
import "testing"
import (
"strings"
"testing"
)
// Equal a generic function to test equivalence between two values
// of the same type
@ -16,3 +19,12 @@ func Equal[T comparable](t *testing.T, actual, expected T) {
t.Errorf("got: %v; want %v", actual, expected)
}
}
func StringContains(t *testing.T, actual, expectedSubstring string) {
t.Helper()
if !strings.Contains(actual, expectedSubstring) {
t.Errorf("got: %q; expected to contain %q", actual, expectedSubstring)
}
}

@ -23,6 +23,12 @@ func (s *Snippet) GetTitle() {
return
}
type SnippetServiceInterface interface {
Insert(title, content string, expiresAt int) (int, error)
Get(id int) (Snippet, error)
Lastest() ([]Snippet, error)
}
type SnippetService struct {
DB *sql.DB
}

@ -20,6 +20,12 @@ type User struct {
UpdatedAt time.Time
}
type UserServiceInterface interface {
Insert(name, email, password string) (int, error)
Authenticate(email, password string) (int, error)
Exists(id int) (bool, error)
}
// TODD add logger to service
type UserService struct {
DB *sql.DB

@ -15,7 +15,7 @@ import (
)
// TODO function should accept and a pointer to an interface allowing for mocking in tests.
func handleHome(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
func handleHome(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService model.SnippetServiceInterface) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Retrieve Snippets from DB
@ -63,7 +63,7 @@ func handleHome(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager,
})
}
func handleSnippetView(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
func handleSnippetView(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, snippetService model.SnippetServiceInterface) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
@ -151,7 +151,7 @@ func handleSnippetCreateGet(tc *TemplateCache, sm *scs.SessionManager) http.Hand
// snippetCreate handles display of the form used to create snippets
//
// curl -iL -d "" http://localhost:5001/snippet/create
func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder *form.Decoder, sm *scs.SessionManager, snippetService *model.SnippetService) http.Handler {
func handleSnippetCreatePost(logger *slog.Logger, tc *TemplateCache, formDecoder *form.Decoder, sm *scs.SessionManager, snippetService model.SnippetServiceInterface) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// example of a custom header. Must be done before calling WriteHeader
@ -291,7 +291,7 @@ func handleUserSignupGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler
})
}
func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decoder, sm *scs.SessionManager, userService *model.UserService) http.Handler {
func handleUserSignupPost(logger *slog.Logger, tc *TemplateCache, fd *form.Decoder, sm *scs.SessionManager, userService model.UserServiceInterface) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check that the provided name, email address and password are not blank.
@ -355,7 +355,7 @@ func handleUserLoginGet(tc *TemplateCache, sm *scs.SessionManager) http.Handler
})
}
func handleUserLoginPost(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, fd *form.Decoder, userService *model.UserService) http.Handler {
func handleUserLoginPost(logger *slog.Logger, tc *TemplateCache, sm *scs.SessionManager, fd *form.Decoder, userService model.UserServiceInterface) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// parse form
err := r.ParseForm()

@ -139,7 +139,7 @@ func NoSurfMiddleware(next http.Handler) http.Handler {
return csrfHandler
}
func AuthenticateMiddleware(next http.Handler, sm *scs.SessionManager, userService *model.UserService) http.Handler {
func AuthenticateMiddleware(next http.Handler, sm *scs.SessionManager, userService model.UserServiceInterface) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := sm.GetInt(r.Context(), "authenticatedUserID")

@ -1,7 +1,6 @@
package server
import (
"database/sql"
"log/slog"
"git.runcible.io/learning/ratchet/internal/model"
@ -13,19 +12,19 @@ type RatchetApp struct {
logger *slog.Logger
templateCache *TemplateCache
//Services used by HTTP routes
snippetService *model.SnippetService
userService *model.UserService
snippetService model.SnippetServiceInterface
userService model.UserServiceInterface
formDecoder *form.Decoder
sessionManager *scs.SessionManager
}
// TODO this function presents some challenges because it both instantiates new data objects
// and configures route / middleware setup
func NewRatchetApp(logger *slog.Logger, tc *TemplateCache, db *sql.DB, sm *scs.SessionManager) *RatchetApp {
func NewRatchetApp(logger *slog.Logger, tc *TemplateCache, snippetService model.SnippetServiceInterface, userService model.UserServiceInterface, sm *scs.SessionManager) *RatchetApp {
rs := new(RatchetApp)
rs.logger = logger
rs.snippetService = &model.SnippetService{DB: db}
rs.userService = &model.UserService{DB: db}
rs.snippetService = snippetService
rs.userService = userService
rs.formDecoder = form.NewDecoder()
rs.templateCache = tc
rs.sessionManager = sm

@ -180,7 +180,6 @@ func renderTemplate(w http.ResponseWriter, r *http.Request, tc *TemplateCache, s
serverError(w, r, err)
return
}
w.Header().Set("Content-Length", "this isn't an integer!")
w.WriteHeader(status)
buf.WriteTo(w)

Loading…
Cancel
Save