diff --git a/cmd/web/handlers_test.go b/cmd/web/handlers_test.go index ff090e2..c3dc007 100644 --- a/cmd/web/handlers_test.go +++ b/cmd/web/handlers_test.go @@ -18,3 +18,61 @@ func TestPing(t *testing.T) { assert.Equal(t, statusCode, http.StatusOK) assert.Equal(t, body, "OK") } + +func TestSnippetView(t *testing.T) { + 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: http.StatusOK, + wantBody: "An old silent pond...", + }, + { + name: "Non-existend ID", + urlPath: "/snippet/view/2", + wantCode: http.StatusNotFound, + }, + { + name: "Negative ID", + urlPath: "/snippet/view/-1", + wantCode: http.StatusNotFound, + }, + { + name: "Decimal ID", + urlPath: "/snippet/view/1.23", + wantCode: http.StatusNotFound, + }, + { + name: "String ID", + urlPath: "/snippet/view/foo", + wantCode: http.StatusNotFound, + }, + { + name: "Empty ID", + urlPath: "/snippet/view/", + wantCode: http.StatusNotFound, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + code, _, body := ts.get(t, tt.urlPath) + + assert.Equal(t, code, tt.wantCode) + + if tt.wantBody != "" { + assert.StringContains(t, body, tt.wantBody) + } + }) + } +} diff --git a/cmd/web/main.go b/cmd/web/main.go index c0d5855..203b754 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -21,8 +21,8 @@ import ( type application struct { errorLog *log.Logger infoLog *log.Logger - snippets *models.SnippetModel - users *models.UserModel + snippets models.SnippetModelInterface + users models.UserModelInterface templateCache map[string]*template.Template formDecoder *form.Decoder sessionManager *scs.SessionManager diff --git a/cmd/web/testutils_test.go b/cmd/web/testutils_test.go index 5e3c4af..01cdc88 100644 --- a/cmd/web/testutils_test.go +++ b/cmd/web/testutils_test.go @@ -8,12 +8,33 @@ import ( "net/http/cookiejar" "net/http/httptest" "testing" + "time" + + "gitea.local.lab/Lbenedar/snippetbox/internal/models/mocks" + "github.com/alexedwards/scs/v2" + "github.com/go-playground/form/v4" ) func newTestApplication(t *testing.T) *application { + templateCache, err := newTemplateCache() + if err != nil { + t.Fatal(err) + } + + formDecoder := form.NewDecoder() + + sessionManager := scs.New() + sessionManager.Lifetime = 12 * time.Hour + sessionManager.Cookie.Secure = true + return &application{ - errorLog: log.New(io.Discard, "", 0), - infoLog: log.New(io.Discard, "", 0), + errorLog: log.New(io.Discard, "", 0), + infoLog: log.New(io.Discard, "", 0), + snippets: &mocks.SnippetModel{}, + users: &mocks.UserModel{}, + templateCache: templateCache, + formDecoder: formDecoder, + sessionManager: sessionManager, } } diff --git a/internal/assert/assert.go b/internal/assert/assert.go index c3a0c09..ee89af1 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -1,6 +1,9 @@ package assert -import "testing" +import ( + "strings" + "testing" +) func Equal[T comparable](t *testing.T, actual, expected T) { t.Helper() @@ -9,3 +12,11 @@ func Equal[T comparable](t *testing.T, actual, expected T) { t.Errorf("got: %v; want: %v", actual, expected) } } + +func StringContains(t *testing.T, actual, expectedString string) { + t.Helper() + + if !strings.Contains(actual, expectedString) { + t.Errorf("got: %q; expected to contain: %q", actual, expectedString) + } +} diff --git a/internal/models/mocks/snippets.go b/internal/models/mocks/snippets.go new file mode 100644 index 0000000..8981993 --- /dev/null +++ b/internal/models/mocks/snippets.go @@ -0,0 +1,34 @@ +package mocks + +import ( + "time" + + "gitea.local.lab/Lbenedar/snippetbox/internal/models" +) + +var mockSnippet = &models.Snippet{ + ID: 1, + Title: "An old silent pond", + Content: "An old silent pond...", + Created: time.Now(), + Expires: time.Now(), +} + +type SnippetModel struct{} + +func (m *SnippetModel) Insert(title string, content string, expires int) (int, error) { + return 2, nil +} + +func (m *SnippetModel) Get(id int) (*models.Snippet, error) { + switch id { + case 1: + return mockSnippet, nil + default: + return nil, models.ErrNoRecord + } +} + +func (m *SnippetModel) Latest() ([]*models.Snippet, error) { + return []*models.Snippet{mockSnippet}, nil +} diff --git a/internal/models/mocks/users.go b/internal/models/mocks/users.go new file mode 100644 index 0000000..6e1b6b7 --- /dev/null +++ b/internal/models/mocks/users.go @@ -0,0 +1,31 @@ +package mocks + +import "gitea.local.lab/Lbenedar/snippetbox/internal/models" + +type UserModel struct{} + +func (m *UserModel) Insert(name, email, password string) error { + switch email { + case "dupe@example.com": + return models.ErrDuplicateEmail + default: + return nil + } +} + +func (m *UserModel) Authenticate(email, password string) (int, error) { + if email == "alice@example.com" && password == "pa$$word" { + return 1, nil + } + + return 0, models.ErrInvalidCredentials +} + +func (m *UserModel) Exists(id int) (bool, error) { + switch id { + case 1: + return true, nil + default: + return false, nil + } +} diff --git a/internal/models/snippets.go b/internal/models/snippets.go index 0b0fdb4..6c055ed 100644 --- a/internal/models/snippets.go +++ b/internal/models/snippets.go @@ -13,7 +13,11 @@ type Snippet struct { Created time.Time Expires time.Time } - +type SnippetModelInterface interface { + Insert(title string, content string, expires int) (int, error) + Get(id int) (*Snippet, error) + Latest() ([]*Snippet, error) +} type SnippetModel struct { DB *sql.DB } diff --git a/internal/models/users.go b/internal/models/users.go index a65fa4d..7a61dac 100644 --- a/internal/models/users.go +++ b/internal/models/users.go @@ -18,6 +18,12 @@ type User struct { Created time.Time } +type UserModelInterface interface { + Insert(name, email, password string) error + Authenticate(email, password string) (int, error) + Exists(id int) (bool, error) +} + type UserModel struct { DB *sql.DB }