Assignment 4: add unit, integration, and e2e tests; refactor for testability

This commit is contained in:
angelicatesvara07-crypto
2026-10-03 19:09:40 +07:00
parent 4ac21847fc
commit 3945920abb
24 changed files with 2753 additions and 147 deletions
+292
View File
@@ -0,0 +1,292 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"5803025015/w04/internal/auth"
"5803025015/w04/internal/models"
)
// ---------- helpers ----------
func setupAuthHandler() (*AuthHandler, *fakeUserStore, *fakeSessionStore) {
us := newFakeUserStore()
ss := newFakeSessionStore()
return NewAuthHandler(us, ss), us, ss
}
func doRequest(
h http.HandlerFunc,
method, path string,
body []byte,
headers map[string]string,
) *httptest.ResponseRecorder {
req := httptest.NewRequest(method, path, bytes.NewReader(body))
if headers != nil {
for k, v := range headers {
req.Header.Set(k, v)
}
}
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
func assertContentTypeJSON(t *testing.T, rec *httptest.ResponseRecorder) {
t.Helper()
if rec.Code == http.StatusNoContent {
return
}
if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
t.Fatalf("want Content-Type application/json, got %q (status=%d, body=%q)",
ct, rec.Code, rec.Body.String())
}
}
func decodeMap(t *testing.T, body []byte) map[string]any {
t.Helper()
var m map[string]any
if err := json.Unmarshal(body, &m); err != nil {
t.Fatalf("body not valid JSON: %v; body=%q", err, string(body))
}
return m
}
// ---------- TestRegister ----------
func TestRegister(t *testing.T) {
t.Run("success -> 201 with id and username", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"alice","password":"secret123"}`), nil)
if rec.Code != http.StatusCreated {
t.Fatalf("want 201, got %d (body=%s)", rec.Code, rec.Body.String())
}
assertContentTypeJSON(t, rec)
m := decodeMap(t, rec.Body.Bytes())
if m["id"] == nil || m["id"] == "" {
t.Fatalf("missing id in response: %v", m)
}
if m["username"] != "alice" {
t.Fatalf("want username alice, got %v", m["username"])
}
})
t.Run("duplicate username -> 409", func(t *testing.T) {
h, _, _ := setupAuthHandler()
body := []byte(`{"username":"alice","password":"secret123"}`)
_ = doRequest(h.Register, http.MethodPost, "/register", body, nil)
rec := doRequest(h.Register, http.MethodPost, "/register", body, nil)
if rec.Code != http.StatusConflict {
t.Fatalf("want 409, got %d (body=%s)", rec.Code, rec.Body.String())
}
assertContentTypeJSON(t, rec)
})
t.Run("empty username -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"","password":"secret123"}`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("empty password -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"alice","password":""}`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("username of 2 chars -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"ab","password":"secret123"}`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
})
t.Run("username of 33 chars -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
long := strings.Repeat("a", 33)
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"`+long+`","password":"secret123"}`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
})
t.Run("malformed JSON -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"alice"`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("success body has no password / password_hash key", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Register, http.MethodPost, "/register",
[]byte(`{"username":"alice","password":"secret123"}`), nil)
if rec.Code != http.StatusCreated {
t.Fatalf("want 201, got %d", rec.Code)
}
raw := rec.Body.String()
if strings.Contains(raw, "password") {
t.Fatalf("body should not contain 'password': %s", raw)
}
if strings.Contains(raw, "secret123") {
t.Fatalf("body leaks plaintext password: %s", raw)
}
m := decodeMap(t, rec.Body.Bytes())
if _, ok := m["password"]; ok {
t.Fatal("response has 'password' key")
}
if _, ok := m["password_hash"]; ok {
t.Fatal("response has 'password_hash' key")
}
})
}
// ---------- TestLogin ----------
func TestLogin(t *testing.T) {
t.Run("success -> 200 with non-empty session_token", func(t *testing.T) {
h, us, _ := setupAuthHandler()
hash, _ := auth.HashPassword("secret123")
_ = us.Create(nil, &models.User{ID: "u1", Username: "alice", PasswordHash: hash})
rec := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"alice","password":"secret123"}`), nil)
if rec.Code != http.StatusOK {
t.Fatalf("want 200, got %d (body=%s)", rec.Code, rec.Body.String())
}
assertContentTypeJSON(t, rec)
m := decodeMap(t, rec.Body.Bytes())
tok, _ := m["session_token"].(string)
if tok == "" {
t.Fatalf("want non-empty session_token, got %v", m)
}
})
t.Run("wrong password -> 401", func(t *testing.T) {
h, us, _ := setupAuthHandler()
hash, _ := auth.HashPassword("secret123")
_ = us.Create(nil, &models.User{ID: "u1", Username: "alice", PasswordHash: hash})
rec := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"alice","password":"wrong"}`), nil)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("unknown username -> 401", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"ghost","password":"whatever"}`), nil)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("wrong password and unknown user bodies are byte-for-byte identical", func(t *testing.T) {
h, us, _ := setupAuthHandler()
hash, _ := auth.HashPassword("secret123")
_ = us.Create(nil, &models.User{ID: "u1", Username: "alice", PasswordHash: hash})
rec1 := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"alice","password":"wrong"}`), nil)
rec2 := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"ghost","password":"whatever"}`), nil)
if rec1.Code != rec2.Code {
t.Fatalf("status codes differ: %d vs %d", rec1.Code, rec2.Code)
}
if !bytes.Equal(rec1.Body.Bytes(), rec2.Body.Bytes()) {
t.Fatalf("bodies differ:\n1=%q\n2=%q", rec1.Body.String(), rec2.Body.String())
}
})
t.Run("malformed JSON -> 400", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Login, http.MethodPost, "/login",
[]byte(`{"username":"alice"`), nil)
if rec.Code != http.StatusBadRequest {
t.Fatalf("want 400, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
}
// ---------- TestLogout ----------
func TestLogout(t *testing.T) {
t.Run("valid token -> 204 empty body, session removed", func(t *testing.T) {
h, _, ss := setupAuthHandler()
_ = ss.Create(nil, &models.Session{Token: "tok-1", UserID: "u1"})
rec := doRequest(h.Logout, http.MethodPost, "/logout", nil,
map[string]string{"Authorization": "Bearer tok-1"})
if rec.Code != http.StatusNoContent {
t.Fatalf("want 204, got %d (body=%s)", rec.Code, rec.Body.String())
}
if rec.Body.Len() != 0 {
t.Fatalf("want empty body, got %q", rec.Body.String())
}
if ss.HasToken("tok-1") {
t.Fatal("session should have been deleted")
}
})
t.Run("missing token -> 401", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Logout, http.MethodPost, "/logout", nil, nil)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("unknown token -> 401", func(t *testing.T) {
h, _, _ := setupAuthHandler()
rec := doRequest(h.Logout, http.MethodPost, "/logout", nil,
map[string]string{"Authorization": "Bearer ghost"})
if rec.Code != http.StatusUnauthorized {
t.Fatalf("want 401, got %d", rec.Code)
}
assertContentTypeJSON(t, rec)
})
t.Run("second logout with same token -> 401", func(t *testing.T) {
h, _, ss := setupAuthHandler()
_ = ss.Create(nil, &models.Session{Token: "tok-2", UserID: "u1"})
rec1 := doRequest(h.Logout, http.MethodPost, "/logout", nil,
map[string]string{"Authorization": "Bearer tok-2"})
if rec1.Code != http.StatusNoContent {
t.Fatalf("first logout: want 204, got %d", rec1.Code)
}
rec2 := doRequest(h.Logout, http.MethodPost, "/logout", nil,
map[string]string{"Authorization": "Bearer tok-2"})
if rec2.Code != http.StatusUnauthorized {
t.Fatalf("second logout: want 401, got %d", rec2.Code)
}
assertContentTypeJSON(t, rec2)
})
}