Agar suatu halaman hanya dapat diakses oleh authenticated user, kita akan gunakan middleware.
Buka file cmd/web/middleware.go, lalu tambahkan fungsi auth() seperti berikut:
package main
import (
"context"
"fmt"
"net"
"net/http"
)
type contextKey string
const contextUserKey contextKey = "user_ip"
func (app *application) ipFromContext(ctx context.Context) string {
return ctx.Value(contextUserKey).(string)
}
func (app *application) addIPToContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var ctx = context.Background()
// get the ip (as accurately as possible)
ip, err := getIP(r)
if err != nil {
ip, _, _ = net.SplitHostPort(r.RemoteAddr)
if len(ip) == 0 {
ip = "unknown"
}
ctx = context.WithValue(r.Context(), contextUserKey, ip)
} else {
ctx = context.WithValue(r.Context(), contextUserKey, ip)
}
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getIP(r *http.Request) (string, error) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return "unknown", err
}
userIP := net.ParseIP(ip)
if userIP == nil {
return "", fmt.Errorf("userip: %q is not IP:port", r.RemoteAddr)
}
forward := r.Header.Get("X-Forwarded-For")
if len(forward) > 0 {
ip = forward
}
if len(ip) == 0 {
ip = "forward"
}
return ip, nil
}
func (app *application) auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !app.Session.Exists(r.Context(), "user") {
app.Session.Put(r.Context(), "error", "Login first")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
next.ServeHTTP(w, r)
})
}
Kemudian registrasikan middleware pada cmd/web/routes.go
package main
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func (app *application) routes() http.Handler {
mux := chi.NewRouter()
//register middleware
mux.Use(middleware.Recoverer)
mux.Use(app.addIPToContext)
mux.Use(app.Session.LoadAndSave) //middleware yang disediakan dari session manager
//register routes
mux.Get("/", app.Home)
mux.Post("/login", app.Login)
//gunakan middleware auth untuk route user profile
mux.Route("/user", func(mux chi.Router) {
mux.Use(app.auth)
mux.Get("/profile", app.Profile)
})
//static assets
fileServer := http.FileServer(http.Dir("./static"))
mux.Handle("/static/*", http.StripPrefix("/static/", fileServer))
return mux
}
Jika kita coba akses http://localhost:8080/user/profile tanpa melakukan login, sesuai ekspektasi, kita akan diredirect ke halaman login dengan message Login first.

Selanjutnya kita buat fungsi test, buka file cmd/web/middleware_test.go, lalu tambahkan fungsi Test_app_auth() seperti berikut.
package main
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"webapp/pkg/data"
)
func Test_application_addIPToContext(t *testing.T) {
tests := []struct {
headerName string
heaaderValue string
addr string
emptyaddr bool
}{
{"", "", "", false},
{"", "", "", true},
{"X-Forwarded-For", "192.163.1.3", "", false},
{"", "", "hello:skillplus", false},
}
//menggunakan environment variable
//var app application
//create dummy handler
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
//check whether value exists in the context
val := r.Context().Value(contextUserKey)
if val == nil {
t.Error(contextUserKey, "not present")
}
//make sure get the string back
ip, ok := val.(string)
if !ok {
t.Error("not string")
}
t.Log(ip)
})
for _, e := range tests {
//create handler for test
handlerToTest := app.addIPToContext(nextHandler)
req := httptest.NewRequest("GET", "http://testing/", nil)
if e.emptyaddr {
req.RemoteAddr = ""
}
if len(e.headerName) > 0 {
req.Header.Add(e.headerName, e.heaaderValue)
}
if len(e.addr) > 0 {
req.RemoteAddr = e.addr
}
handlerToTest.ServeHTTP(httptest.NewRecorder(), req)
}
}
func Test_application_ipFromContext(t *testing.T) {
//menggunakan environment variable
//var app application
//get a context
ctx := context.Background()
//put somtehing in the context
ctx = context.WithValue(ctx, contextUserKey, "random")
//call the function
ip := app.ipFromContext(ctx)
//perform test
if !strings.EqualFold("random", ip) {
t.Error("wrong value returned from context")
}
}
func Test_app_auth(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
})
var tests = []struct {
name string
isAuth bool
}{
{"logged in", true},
{"not logged in", false},
}
for _, e := range tests {
handlerToTest := app.auth(nextHandler)
req := httptest.NewRequest("GET", "http://testing", nil)
req = addCtxSessToReq(req, app)
if e.isAuth {
app.Session.Put(req.Context(), "user", data.User{ID: 1})
}
rr := httptest.NewRecorder()
handlerToTest.ServeHTTP(rr, req)
if e.isAuth && rr.Code != http.StatusOK {
t.Errorf("%s: expected status code of 200 but got %d", e.name, rr.Code)
}
if !e.isAuth && rr.Code != http.StatusTemporaryRedirect {
t.Errorf("%s: expected status code of 307 but got %d", e.name, rr.Code)
}
}
}
Jika kita jalankan test, sesuai ekspektasi, test berhasil.
cmd\web $ go test -v -run Test_app_auth
2022/10/25 17:55:16 Connected to Postgres!
=== RUN Test_app_auth
--- PASS: Test_app_auth (0.00s)
PASS
ok webapp/cmd/web 0.685s
Sampai disini kita sudah menambahkan middleware authentication untuk mencegah halaman tertentu diakses tanpa credentials. Kita juga sudah menambahkan fungsi test untuk middleware tersebut.
Berikutnya kita akan melakukan houskeeping dengan menambahkan route “/user/profile” pada cmd/web/routes_test.go
package main
import (
"net/http"
"strings"
"testing"
"github.com/go-chi/chi/v5"
)
func Test_application_routes(t *testing.T) {
var registered = []struct {
route string
method string
}{
{"/", "GET"},
{"/login", "POST"},
{"/user/profile", "GET"},
{"/static/*", "GET"},
}
mux := app.routes()
chiRoutes := mux.(chi.Routes)
for _, route := range registered {
//check if the route exists
if !routeExists(route.route, route.method, chiRoutes) {
t.Errorf("route %s is not registered", route.route)
}
}
}
func routeExists(testRoute, testMethod string, chiRoutes chi.Routes) bool {
found := false
_ = chi.Walk(chiRoutes, func(method string, route string, handler http.Handler, middleware ...func(http.Handler) http.Handler) error {
if strings.EqualFold(method, testMethod) && strings.EqualFold(route, testRoute) {
found = true
}
return nil
})
return found
}
Housekeeping selanjutnya adalah, memperbaiki Test_application_handlers pada cmd/web/handlers_test.go agar memenuhi url yang harus ditest.
Kita akan mengubah struct table test, serta menambahkan data pada table test dan menambahkan test server kedua untuk melakukan pemeriksaan last status code , Berikut code setelah dimodifikasi.
package main
import (
"context"
"crypto/tls"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
func Test_application_handlers(t *testing.T) {
var theTests = []struct {
name string
url string
expectedStatusCode int
expectedURL string
expectedFirstStatCOde int
}{
{"home", "/", http.StatusOK, "/", http.StatusOK},
{"404", "/qwerty", http.StatusNotFound, "/qwerty", http.StatusNotFound},
{"profile", "/user/profile", http.StatusOK, "/", http.StatusTemporaryRedirect},
}
routes := app.routes()
//create a test server
ts := httptest.NewTLSServer(routes)
defer ts.Close()
//create test server 2 untuk mendapatkan http statuscode pertama.
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
//testing
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)
}
//check jika non credentials user masuk maka diharapkan statuscode adalah StatusTemporaryRedirect
resp2, _ := client.Get(ts.URL + e.url)
if resp2.StatusCode != e.expectedFirstStatCOde {
t.Errorf("%s: expected first return stat code %d but go %d", e.name, e.expectedFirstStatCOde, resp2.StatusCode)
}
}
}
func TestAppHome(t *testing.T) {
var tests = []struct {
name string
putInSession string
expctedHTML string
}{
{"First Visit", "", "<small>Session:"},
{"Second Visit", "skillplus", "<small>Session: skillplus"},
}
for _, e := range tests {
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.expctedHTML) {
t.Errorf("%s: did not find %s in response body", e.name, e.expctedHTML)
}
}
}
func TestApp_renderWBadTemplate(t *testing.T) {
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")
}
templatePath = "./../../templates/"
}
func addCtxSessToReq(req *http.Request, app application) *http.Request {
req = req.WithContext(context.WithValue(req.Context(), contextUserKey, "unknown"))
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
expectedStatCode int
expectedLoc string
}{
{
name: "Valid Login",
postedData: url.Values{"email": {"admin@example.com"}, "password": {"secret"}},
expectedStatCode: http.StatusSeeOther,
expectedLoc: "/user/profile",
},
{
name: "Missing form data",
postedData: url.Values{"email": {""}, "password": {""}},
expectedStatCode: http.StatusSeeOther,
expectedLoc: "/",
},
{
name: "User not found",
postedData: url.Values{"email": {"me@skillplus.web.id"}, "password": {"123"}},
expectedStatCode: http.StatusSeeOther,
expectedLoc: "/",
},
{
name: "Bad Credentials",
postedData: url.Values{"email": {"admin@example.com"}, "password": {"123"}},
expectedStatCode: 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.expectedStatCode {
t.Errorf("%s: returned wrong stat code: expected %d, but got %d", e.name, e.expectedStatCode, 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)
}
}
}
JIka kita jalankan test dari directory cmd/web/
cmd\web> go test -v -run Test_application_handlers
2022/10/26 16:52:35 Connected to Postgres!
=== RUN Test_application_handlers
--- PASS: Test_application_handlers (0.25s)
PASS
ok webapp/cmd/web 1.258s
Sesuai ekspektasi, test berhasil dan sudah mengcover route yang ada.