Sebelum membuat fungsi test, kita perlu perbaiki file cmd/web/handlers.go dengan menambahkan variable uploadpath dan digunakan pada fungsi UploadProfilePic, berikut kode akhir dari handlers.go.
package main
import (
"fmt"
"html/template"
"io"
"log"
"net/http"
"os"
"path"
"path/filepath"
"time"
"webapp/pkg/data"
)
var templatePath = "./templates/"
var uploadPath = "./static/img"
func (app *application) Home(w http.ResponseWriter, r *http.Request) {
var td = make(map[string]any)
if app.Session.Exists(r.Context(), "test") {
msg := app.Session.GetString(r.Context(), "test")
td["test"] = msg
} else {
app.Session.Put(r.Context(), "test", "Page visit at "+time.Now().UTC().String())
}
_ = app.render(w, r, "home.page.gohtml", &TemplateData{Data: td})
}
func (app *application) Profile(w http.ResponseWriter, r *http.Request) {
_ = app.render(w, r, "profile.page.gohtml", &TemplateData{})
}
type TemplateData struct {
IP string
Data map[string]any
Error string
Flash string
User data.User
}
func (app *application) render(w http.ResponseWriter, r *http.Request, t string, td *TemplateData) error {
//parse template
parsedTemplate, err := template.ParseFiles(path.Join(templatePath, t), path.Join(templatePath, "base.layout.gohtml"))
if err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return err
}
//gunakan middleware yang telah kita buat.
td.IP = app.ipFromContext(r.Context())
td.Error = app.Session.PopString(r.Context(), "error")
td.Flash = app.Session.PopString(r.Context(), "flash")
if app.Session.Exists(r.Context(), "user") {
td.User = app.Session.Get(r.Context(), "user").(data.User)
}
//execute template
err = parsedTemplate.Execute(w, td)
if err != nil {
return err
}
return nil
}
func (app *application) Login(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Println(err)
http.Error(w, "bad request", http.StatusBadRequest)
return
}
//validation goes here
form := NewForm(r.PostForm)
form.Required("email", "password")
if !form.Valid() {
//redirect to login page with error message.
app.Session.Put(r.Context(), "error", "Invalid login credentials")
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
email := r.Form.Get("email")
password := r.Form.Get("password")
user, err := app.DB.GetUserByEmail(email)
if err != nil {
//redirect to login page with error message.
app.Session.Put(r.Context(), "error", "Invalid login")
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
//authenticate the user
// if not authenticated, redirect with error
if !app.authenticate(r, user, password) {
app.Session.Put(r.Context(), "error", "Invalid login")
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
//prevent fixation attack
_ = app.Session.RenewToken(r.Context())
//redirect to other page
app.Session.Put(r.Context(), "flash", "Successfully loged in")
http.Redirect(w, r, "user/profile", http.StatusSeeOther)
}
func (app *application) authenticate(r *http.Request, user *data.User, password string) bool {
if valid, err := user.PasswordMatches(password); err != nil || !valid {
return false
}
app.Session.Put(r.Context(), "user", user)
return true
}
func (app *application) UploadProfilePic(w http.ResponseWriter, r *http.Request) {
// call a function that extracts a file from an upload (request)
files, err := app.UploadFiles(r, uploadPath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// get the user from the session
user := app.Session.Get(r.Context(), "user").(data.User)
// create a var of type data.UserImage
var i = data.UserImage{
UserID: user.ID,
FileName: files[0].OriginalFileName,
}
// insert the user image into user_images
_, err = app.DB.InsertUserImage(i)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// refresh the sessional variable "user"
updatedUser, err := app.DB.GetUser(user.ID)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
app.Session.Put(r.Context(), "user", updatedUser)
// redirect back to profile page
http.Redirect(w, r, "/user/profile", http.StatusSeeOther)
}
type UploadedFile struct {
OriginalFileName string
FileSize int64
}
func (app *application) UploadFiles(r *http.Request, uploadDir string) ([]*UploadedFile, error) {
var uploadedFiles []*UploadedFile
err := r.ParseMultipartForm(int64(1024 * 1024 * 5))
if err != nil {
return nil, fmt.Errorf("the uploaded file is too big, and must be less than %d bytes", 1024*1024*5)
}
for _, fHeaders := range r.MultipartForm.File {
for _, hdr := range fHeaders {
uploadedFiles, err = func(uploadedFiles []*UploadedFile) ([]*UploadedFile, error) {
var uploadedFile UploadedFile
infile, err := hdr.Open()
if err != nil {
return nil, err
}
defer infile.Close()
uploadedFile.OriginalFileName = hdr.Filename
var outfile *os.File
defer outfile.Close()
if outfile, err = os.Create(filepath.Join(uploadDir, uploadedFile.OriginalFileName)); nil != err {
return nil, err
} else {
fileSize, err := io.Copy(outfile, infile)
if err != nil {
return nil, err
}
uploadedFile.FileSize = fileSize
}
uploadedFiles = append(uploadedFiles, &uploadedFile)
return uploadedFiles, nil
}(uploadedFiles)
if err != nil {
return uploadedFiles, err
}
}
}
return uploadedFiles, nil
}
Selanjutnya kita buka file cmd/web/handlers_test.go, lalu tambahkan fungsi test untuk UploadProfilePic.
package main
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"image"
"image/png"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"strings"
"sync"
"testing"
"webapp/pkg/data"
)
func Test_application_handlers(t *testing.T) {
var theTests = []struct {
name string
url string
expectedStatusCode int
expectedURL string
expectedFirstStatusCode int
}{
{"home", "/", http.StatusOK, "/", http.StatusOK},
{"404", "/fish", http.StatusNotFound, "/fish", http.StatusNotFound},
{"profile", "/user/profile", http.StatusOK, "/", http.StatusTemporaryRedirect},
}
routes := app.routes()
// create a test server
ts := httptest.NewTLSServer(routes)
defer ts.Close()
// If we want to get the first status code, we have to create our
// own http client with a custom CheckRedirect function, and limit
// it ot the first response. For testing, we also need to
// specify a custom Transport field which accepts insecure
// https certificates. First create the custom transport.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
// Then create the custom client.
client := &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
// range through test data
for _, e := range theTests {
resp, err := ts.Client().Get(ts.URL + e.url)
if err != nil {
t.Log(err)
t.Fatal(err)
}
if resp.StatusCode != e.expectedStatusCode {
t.Errorf("for %s: expected status %d, but got %d", e.name, e.expectedStatusCode, resp.StatusCode)
}
if resp.Request.URL.Path != e.expectedURL {
t.Errorf("%s: expected final url of %s but got %s", e.name, e.expectedURL, resp.Request.URL.Path)
}
// Call the test server using our custom http client
// which does not follow redirects, and which has a custom
// transport.
resp2, _ := client.Get(ts.URL + e.url)
if resp2.StatusCode != e.expectedFirstStatusCode {
t.Errorf("%s: expected first returned status code to be %d but got %d", e.name, e.expectedFirstStatusCode, resp2.StatusCode)
}
}
}
func TestAppHome(t *testing.T) {
var tests = []struct {
name string
putInSession string
expectedHTML string
}{
{"first visit", "", "<small>Session:"},
{"second visit", "hello, skillplus!", "<small>Session: hello, skillplus!"},
}
for _, e := range tests {
// create a request
req, _ := http.NewRequest("GET", "/", nil)
req = addCtxSessToReq(req, app)
_ = app.Session.Destroy(req.Context())
if e.putInSession != "" {
app.Session.Put(req.Context(), "test", e.putInSession)
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.Home)
handler.ServeHTTP(rr, req)
// check status code
if rr.Code != http.StatusOK {
t.Errorf("TestAppHome returned wrong status code; expected 200 but got %d", rr.Code)
}
body, _ := io.ReadAll(rr.Body)
if !strings.Contains(string(body), e.expectedHTML) {
t.Errorf("%s: did not find %s in response body", e.name, e.expectedHTML)
}
}
}
func TestApp_renderWithBadTemplate(t *testing.T) {
// set pathToTemplates to a location with a bad template
templatePath = "./testdata/"
req, _ := http.NewRequest("GET", "/", nil)
req = addCtxSessToReq(req, app)
rr := httptest.NewRecorder()
err := app.render(rr, req, "bad.page.gohtml", &TemplateData{})
if err == nil {
t.Error("expected error from bad template, but did not get one")
}
templatePath = "./../../templates/"
}
func getCtx(req *http.Request) context.Context {
ctx := context.WithValue(req.Context(), contextUserKey, "unknown")
return ctx
}
func addCtxSessToReq(req *http.Request, app application) *http.Request {
req = req.WithContext(getCtx(req))
ctx, _ := app.Session.Load(req.Context(), req.Header.Get("X-Session"))
return req.WithContext(ctx)
}
func Test_app_Login(t *testing.T) {
var tests = []struct {
name string
postedData url.Values
expectedStatusCode int
expectedLoc string
}{
{
name: "valid login",
postedData: url.Values{
"email": {"admin@example.com"},
"password": {"secret"},
},
expectedStatusCode: http.StatusSeeOther,
expectedLoc: "/user/profile",
},
{
name: "missing form data",
postedData: url.Values{
"email": {""},
"password": {""},
},
expectedStatusCode: http.StatusSeeOther,
expectedLoc: "/",
},
{
name: "user not found",
postedData: url.Values{
"email": {"you@there.com"},
"password": {"password"},
},
expectedStatusCode: http.StatusSeeOther,
expectedLoc: "/",
},
{
name: "bad credentials",
postedData: url.Values{
"email": {"admin@example.com"},
"password": {"password"},
},
expectedStatusCode: http.StatusSeeOther,
expectedLoc: "/",
},
}
for _, e := range tests {
req, _ := http.NewRequest("POST", "/login", strings.NewReader(e.postedData.Encode()))
req = addCtxSessToReq(req, app)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.Login)
handler.ServeHTTP(rr, req)
if rr.Code != e.expectedStatusCode {
t.Errorf("%s: returned wrong status code; expected %d, but got %d", e.name, e.expectedStatusCode, rr.Code)
}
actualLoc, err := rr.Result().Location()
if err == nil {
if actualLoc.String() != e.expectedLoc {
t.Errorf("%s: expected location %s but got %s", e.name, e.expectedLoc, actualLoc.String())
}
} else {
t.Errorf("%s: no location header set", e.name)
}
}
}
func Test_app_UploadFiles(t *testing.T) {
// set up pipes
pr, pw := io.Pipe()
// create a new writer, of type *io.Writer
writer := multipart.NewWriter(pw)
// create a waitgroup, and add 1 to it
wg := &sync.WaitGroup{}
wg.Add(1)
// simulate uploading a file using a goroutine and our writer
go simulatePNGUpload("./testdata/img.png", writer, t, wg)
// read from the pipe which receives data
request := httptest.NewRequest("POST", "/", pr)
request.Header.Add("Content-Type", writer.FormDataContentType())
// call app.UploadFiles
uploadedFiles, err := app.UploadFiles(request, "./testdata/uploads/")
if err != nil {
t.Error(err)
}
// perform our tests
if _, err := os.Stat(fmt.Sprintf("./testdata/uploads/%s", uploadedFiles[0].OriginalFileName)); os.IsNotExist(err) {
t.Errorf("expected file to exist: %s", err.Error())
}
// clean up
_ = os.Remove(fmt.Sprintf("./testdata/uploads/%s", uploadedFiles[0].OriginalFileName))
}
func simulatePNGUpload(fileToUpload string, writer *multipart.Writer, t *testing.T, wg *sync.WaitGroup) {
defer writer.Close()
defer wg.Done()
// create the form data filed 'file' with value being filename
part, err := writer.CreateFormFile("file", path.Base(fileToUpload))
if err != nil {
t.Error(err)
}
// open the actual file
f, err := os.Open(fileToUpload)
if err != nil {
t.Error(err)
}
defer f.Close()
// decode the image
img, _, err := image.Decode(f)
if err != nil {
t.Error("error decoding image:", err)
}
// write the png to our io.Writer
err = png.Encode(part, img)
if err != nil {
t.Error(err)
}
}
func Test_app_UploadProfilePic(t *testing.T) {
uploadPath = "./testdata/uploads"
filePath := "./testdata/img.png"
// specify a field name for the form
fieldName := "file"
// create a bytes.Buffer to act as the request body
body := new(bytes.Buffer)
// create a new writer
mw := multipart.NewWriter(body)
file, err := os.Open(filePath)
if err != nil {
t.Fatal(err)
}
w, err := mw.CreateFormFile(fieldName, filePath)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, file); err != nil {
t.Fatal(err)
}
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req = addCtxSessToReq(req, app)
app.Session.Put(req.Context(), "user", data.User{ID: 1})
req.Header.Add("Content-Type", mw.FormDataContentType())
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.UploadProfilePic)
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusSeeOther {
t.Errorf("wrong status code")
}
_ = os.Remove("./testdata/uploads/img.png")
}
Bila kita jalankan test untuk fungsi diatas, sesuai ekspektasi, test berhasil.
$ go test -v -run Test_app_UploadProfilePic
=== RUN Test_app_UploadProfilePic
--- PASS: Test_app_UploadProfilePic (0.00s)
PASS
ok webapp/cmd/web 0.410s
Pada modul selanjutnya kita akan membahas testing untuk web API.