Nah, semoga Anda dapat membuat fungsi handler serta testnya, jika Ada masalah, silakan gunakan solusi yang kami sediakan sebagai referensi.
File cmd/api/api-handlers.go
package main
import (
"errors"
"net/http"
"strconv"
"time"
"webapp/pkg/data"
"github.com/go-chi/chi/v5"
"github.com/golang-jwt/jwt/v4"
"golang.org/x/crypto/bcrypt"
)
// Credentials is the type used to unmarshal a JSON payload
// during authentication.
type Credentials struct {
Username string `json:"email"`
Password string `json:"password"`
}
// authenticate is the handler used to try to authenticate a user, and
// send them a JWT token if successful.
func (app *application) authenticate(w http.ResponseWriter, r *http.Request) {
var creds Credentials
// read a json payload
err := app.readJSON(w, r, &creds)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// look up the user by email address
user, err := app.DB.GetUserByEmail(creds.Username)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// check password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(creds.Password))
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// generate tokens
tokenPairs, err := app.generateTokenPair(user)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"), http.StatusUnauthorized)
return
}
// send token to user
_ = app.writeJSON(w, http.StatusOK, tokenPairs)
}
// refresh is the handler called to request a new token pair, when
// the jwt token has expired. We expect the refresh token to come
// from a POST request. We validate it, look up the user in the db,
// and if everything is good we send back a new token pair
// as JSON. We also set an http only, secure cookie with the refresh
// token stored inside.
func (app *application) refresh(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
refreshToken := r.Form.Get("refresh_token")
claims := &Claims{}
_, err = jwt.ParseWithClaims(refreshToken, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(app.JWTSecret), nil
})
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
if time.Unix(claims.ExpiresAt.Unix(), 0).Sub(time.Now()) > 30*time.Second {
app.errorJSON(w, errors.New("refresh token does not need renewed yet"), http.StatusTooEarly)
return
}
// get the user id from the claims
userID, err := strconv.Atoi(claims.Subject)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
user, err := app.DB.GetUser(userID)
if err != nil {
app.errorJSON(w, errors.New("unknown user"), http.StatusBadRequest)
return
}
tokenPairs, err := app.generateTokenPair(user)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
http.SetCookie(w, &http.Cookie{
Name: "__Host-refresh_token",
Path: "/",
Value: tokenPairs.RefreshToken,
Expires: time.Now().Add(refreshTokenExpiry),
MaxAge: int(refreshTokenExpiry.Seconds()),
SameSite: http.SameSiteStrictMode,
Domain: "localhost",
HttpOnly: true,
Secure: true,
})
_ = app.writeJSON(w, http.StatusOK, tokenPairs)
}
// allUsers returns a list of all users as JSON
func (app *application) allUsers(w http.ResponseWriter, r *http.Request) {
users, err := app.DB.AllUsers()
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
_ = app.writeJSON(w, http.StatusOK, users)
}
// getUser returns one user as JSON
func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.Atoi(chi.URLParam(r, "userID"))
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
user, err := app.DB.GetUser(userID)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
_ = app.writeJSON(w, http.StatusOK, user)
}
// updateUser updates a user from a JSON payload, and returns just a header
func (app *application) updateUser(w http.ResponseWriter, r *http.Request) {
var user data.User
err := app.readJSON(w, r, &user)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
err = app.DB.UpdateUser(user)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
// deleteUser deletes one user based on ID in URL, and returns a header
func (app *application) deleteUser(w http.ResponseWriter, r *http.Request) {
userID, err := strconv.Atoi(chi.URLParam(r, "userID"))
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
err = app.DB.DeleteUser(userID)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
// insertUser inserts a user using a JSON payload, and returns a header
func (app *application) insertUser(w http.ResponseWriter, r *http.Request) {
var user data.User
err := app.readJSON(w, r, &user)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
_, err = app.DB.InsertUser(user)
if err != nil {
app.errorJSON(w, err, http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
}
File cmd/api/api-handlers_test.go.
package main
import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"webapp/pkg/data"
"github.com/go-chi/chi/v5"
)
func Test_app_authenticate(t *testing.T) {
var theTests = []struct {
name string
requestBody string
expectedStatusCode int
}{
{"valid user", `{"email":"admin@example.com","password":"secret"}`, http.StatusOK},
{"not json", `I'm not JSON`, http.StatusUnauthorized},
{"empty json", `{}`, http.StatusUnauthorized},
{"empty email", `{"email":""}`, http.StatusUnauthorized},
{"empty password", `{"email":"admin@example.com"}`, http.StatusUnauthorized},
{"invalid user", `{"email":"admin@someotherdomain.com","password":"secret"}`, http.StatusUnauthorized},
}
for _, e := range theTests {
var reader io.Reader
reader = strings.NewReader(e.requestBody)
req, _ := http.NewRequest("POST", "/auth", reader)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.authenticate)
handler.ServeHTTP(rr, req)
if e.expectedStatusCode != rr.Code {
t.Errorf("%s: returned wrong status code; expected %d but got %d", e.name, e.expectedStatusCode, rr.Code)
}
}
}
func Test_app_refresh(t *testing.T) {
var tests = []struct {
name string
token string
expectedStatusCode int
resetRefreshTime bool
}{
{"valid", "", http.StatusOK, true},
{"valid but not yet ready to expire", "", http.StatusTooEarly, false},
{"expired token", expiredToken, http.StatusBadRequest, false},
}
testUser := data.User{
ID: 1,
FirstName: "Admin",
LastName: "User",
Email: "admin@example.com",
}
oldRefreshTime := refreshTokenExpiry
for _, e := range tests {
var tkn string
if e.token == "" {
if e.resetRefreshTime {
refreshTokenExpiry = time.Second * 1
}
tokens, _ := app.generateTokenPair(&testUser)
tkn = tokens.RefreshToken
} else {
tkn = e.token
}
postedData := url.Values{
"refresh_token": {tkn},
}
req, _ := http.NewRequest("POST", "/refresh-token", strings.NewReader(postedData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(app.refresh)
handler.ServeHTTP(rr, req)
if rr.Code != e.expectedStatusCode {
t.Errorf("%s: expected status of %d but got %d", e.name, e.expectedStatusCode, rr.Code)
}
refreshTokenExpiry = oldRefreshTime
}
}
func Test_app_userHandlers(t *testing.T) {
var tests = []struct {
name string
method string
json string
paramID string
handler http.HandlerFunc
expectedStatus int
}{
{"allUsers", "GET", "", "", app.allUsers, http.StatusOK},
{"deleteUser", "DELETE", "", "1", app.deleteUser, http.StatusNoContent},
{"deleteUser bad URL param", "DELETE", "", "Y", app.deleteUser, http.StatusBadRequest},
{"getUser valid", "GET", "", "1", app.getUser, http.StatusOK},
{"getUser invalid", "GET", "", "100", app.getUser, http.StatusBadRequest},
{"getUser bad URL param", "GET", "", "Y", app.getUser, http.StatusBadRequest},
{
"updateUser valid",
"PATCH",
`{"id":1,"first_name":"Administrator","last_name":"User","email":"admin@example.com"}`,
"",
app.updateUser,
http.StatusNoContent,
},
{
"updateUser invalid",
"PATCH",
`{"id":100,"first_name":"Administrator","last_name":"User","email":"admin@example.com"}`,
"",
app.updateUser,
http.StatusNoContent,
},
{
"updateUser invalid json",
"PATCH",
`{"id":1,first_name:"Administrator","last_name":"User","email":"admin@example.com"}`,
"",
app.updateUser,
http.StatusBadRequest,
},
{
"insertUser valid",
"PUT",
`{"first_name":"Jack","last_name":"Smith","email":"jack@example.com"}`,
"",
app.insertUser,
http.StatusNoContent,
},
{
"insertUser invalid",
"PUT",
`{"foo":"bar","first_name":"Jack","last_name":"Smith","email":"jack@example.com"}`,
"",
app.insertUser,
http.StatusBadRequest,
},
{
"insertUser invalid json",
"PUT",
`{first_name:"Jack","last_name":"Smith","email":"jack@example.com"}`,
"",
app.insertUser,
http.StatusBadRequest,
},
}
for _, e := range tests {
var req *http.Request
if e.json == "" {
req, _ = http.NewRequest(e.method, "/", nil)
} else {
req, _ = http.NewRequest(e.method, "/", strings.NewReader(e.json))
}
if e.paramID != "" {
chiCtx := chi.NewRouteContext()
chiCtx.URLParams.Add("userID", e.paramID)
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, chiCtx))
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(e.handler)
handler.ServeHTTP(rr, req)
if rr.Code != e.expectedStatus {
t.Errorf("%s: wrong status returned; expected %d but got %d", e.name, e.expectedStatus, rr.Code)
}
}
}
Diperlukan sedikit perbaikan untuk file pkg/repository/dbrepo/users_test.db.
package dbrepo
import (
"database/sql"
"errors"
"time"
"webapp/pkg/data"
)
type TestDBRepo struct{}
func (m *TestDBRepo) Connection() *sql.DB {
return nil
}
// AllUsers returns all users as a slice of *data.User
func (m *TestDBRepo) AllUsers() ([]*data.User, error) {
var users []*data.User
return users, nil
}
// GetUser returns one user by id
func (m *TestDBRepo) GetUser(id int) (*data.User, error) {
var user = data.User{}
if id == 1 {
user = data.User{
ID: 1,
FirstName: "Admin",
LastName: "User",
Email: "admin@example.com",
}
return &user, nil
}
return nil, errors.New("user not found")
}
// GetUserByEmail returns one user by email address
func (m *TestDBRepo) GetUserByEmail(email string) (*data.User, error) {
if email == "admin@example.com" {
user := data.User{
ID: 1,
FirstName: "Admin",
LastName: "User",
Email: "admin@example.com",
Password: "$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK",
IsAdmin: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
return &user, nil
}
return nil, errors.New("not found")
}
// UpdateUser updates one user in the database
func (m *TestDBRepo) UpdateUser(u data.User) error {
return nil
}
// DeleteUser deletes one user from the database, by id
func (m *TestDBRepo) DeleteUser(id int) error {
return nil
}
// InsertUser inserts a new user into the database, and returns the ID of the newly inserted row
func (m *TestDBRepo) InsertUser(user data.User) (int, error) {
return 2, nil
}
// ResetPassword is the method we will use to change a user's password.
func (m *TestDBRepo) ResetPassword(id int, password string) error {
return nil
}
// InsertUserImage inserts a user profile image into the database.
func (m *TestDBRepo) InsertUserImage(i data.UserImage) (int, error) {
return 1, nil
}
Dan modifikasi const menjadi var pada file cmd/api/auth.go
package main
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"webapp/pkg/data"
"github.com/golang-jwt/jwt/v4"
)
var jwtTokenExpiry = time.Minute * 15
var refreshTokenExpiry = time.Hour * 24
type TokenPairs struct {
Token string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type Claims struct {
UserName string `json:"name"`
jwt.RegisteredClaims
}
func (app *application) getTokenFromHeaderAndVerify(w http.ResponseWriter, r *http.Request) (string, *Claims, error) {
// we expect our authorization header to look like this:
// Bearer <token>
// add a header
w.Header().Add("Vary", "Authorization")
// get the authorization header
authHeader := r.Header.Get("Authorization")
// sanity check
if authHeader == "" {
return "", nil, errors.New("no auth header")
}
// split the header on spaces
headerParts := strings.Split(authHeader, " ")
if len(headerParts) != 2 {
return "", nil, errors.New("invalid auth header")
}
// check to see if we have the word "Bearer"
if headerParts[0] != "Bearer" {
return "", nil, errors.New("unauthorized: no Bearer")
}
token := headerParts[1]
// declare an empty Claims variable
claims := &Claims{}
// parse the token with our claims (we read into claims), using our secret (from the receiver)
_, err := jwt.ParseWithClaims(token, claims, func(token *jwt.Token) (interface{}, error) {
// validate the signing algorithm
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexepected signing method: %v", token.Header["alg"])
}
return []byte(app.JWTSecret), nil
})
// check for an error; note that this catches expired tokens as well.
if err != nil {
if strings.HasPrefix(err.Error(), "token is expired by") {
return "", nil, errors.New("expired token")
}
return "", nil, err
}
// make sure that we issued this token
if claims.Issuer != app.Domain {
return "", nil, errors.New("incorrect issuer")
}
// valid token
return token, claims, nil
}
func (app *application) generateTokenPair(user *data.User) (TokenPairs, error) {
// Create the token.
token := jwt.New(jwt.SigningMethodHS256)
// set claims
claims := token.Claims.(jwt.MapClaims)
claims["name"] = fmt.Sprintf("%s %s", user.FirstName, user.LastName)
claims["sub"] = fmt.Sprint(user.ID)
claims["aud"] = app.Domain
claims["iss"] = app.Domain
if user.IsAdmin == 1 {
claims["admin"] = true
} else {
claims["admin"] = false
}
// set the expiry
claims["exp"] = time.Now().Add(jwtTokenExpiry).Unix()
// create the signed token
signedAccessToken, err := token.SignedString([]byte(app.JWTSecret))
if err != nil {
return TokenPairs{}, err
}
// create the refresh token
refreshToken := jwt.New(jwt.SigningMethodHS256)
refreshTokenClaims := refreshToken.Claims.(jwt.MapClaims)
refreshTokenClaims["sub"] = fmt.Sprint(user.ID)
// set expiry; must be longer than jwt expiry
refreshTokenClaims["exp"] = time.Now().Add(refreshTokenExpiry).Unix()
// create signed refresh token
signedRefreshToken, err := refreshToken.SignedString([]byte(app.JWTSecret))
if err != nil {
return TokenPairs{}, err
}
var tokenPairs = TokenPairs{
Token: signedAccessToken,
RefreshToken: signedRefreshToken,
}
return tokenPairs, nil
}
Sampai disini kita sudah membahas cara-cara melakukan testing pada aplikasi CLI sederhana, Web App dan Web API.
Pendekatan yang digunakan dalam tutorial untuk melakukan testing tidaklah mutlak, namun dapat digunakan sebagai referensi atau ide dalam melakukan testing.
Dengan berakhirnya modul ini, tutorial Intro testing dalam Bahasa Go menggunakan Unit Test telah selesai.
Semoga bermanfaat.