Files
w04_20_Yovan_DataBase/internal/handler/auth_handler_test.go
T
2026-10-03 20:28:25 +07:00

266 lines
9.2 KiB
Go

package handler
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"5803025020/w04/internal/auth"
"5803025020/w04/internal/models"
)
func newTestAuthHandler() (*AuthHandler, *fakeUserStore, *fakeSessionStore) {
us := newFakeUserStore()
ss := newFakeSessionStore()
return NewAuthHandler(us, ss), us, ss
}
// doJSONRequest builds a request whose body is either a Go value (marshalled
// to JSON) or a raw string (used as-is, to exercise malformed-JSON cases).
func doJSONRequest(h http.HandlerFunc, method, path string, body any) *httptest.ResponseRecorder {
var raw []byte
switch v := body.(type) {
case string:
raw = []byte(v)
default:
raw, _ = json.Marshal(v)
}
req := httptest.NewRequest(method, path, bytes.NewReader(raw))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h(rec, req)
return rec
}
// assertJSONContentType is used on every handler test subtest that inspects
// a non-204 response, per the spec's blanket requirement.
func assertJSONContentType(t *testing.T, rec *httptest.ResponseRecorder) {
t.Helper()
ct := rec.Header().Get("Content-Type")
if !strings.HasPrefix(ct, "application/json") {
t.Fatalf("expected Content-Type application/json, got %q", ct)
}
}
func TestRegister(t *testing.T) {
t.Run("success returns 201 with id and echoed username", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{
"username": "alice", "password": "password123",
})
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d: %s", rec.Code, rec.Body.String())
}
assertJSONContentType(t, rec)
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("expected JSON body: %v", err)
}
if id, _ := body["id"].(string); id == "" {
t.Fatal("expected non-empty id in response")
}
if body["username"] != "alice" {
t.Fatalf("expected username %q, got %v", "alice", body["username"])
}
})
t.Run("duplicate username returns 409", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "bob", "password": "password123"})
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "bob", "password": "anotherpass"})
if rec.Code != http.StatusConflict {
t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
}
assertJSONContentType(t, rec)
})
t.Run("empty username returns 400", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "", "password": "password123"})
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("empty password returns 400", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "charlie", "password": ""})
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("username of 2 characters returns 400", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "ab", "password": "password123"})
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("username of 33 characters returns 400", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
longUsername := strings.Repeat("a", 33)
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": longUsername, "password": "password123"})
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("malformed JSON returns 400", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", "{not valid json")
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("success body contains no password or password_hash key, nor the plaintext", func(t *testing.T) {
h, _, _ := newTestAuthHandler()
rec := doJSONRequest(h.Register, http.MethodPost, "/register", map[string]string{"username": "dana", "password": "supersecretpw"})
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", rec.Code)
}
assertJSONContentType(t, rec)
lower := strings.ToLower(rec.Body.String())
if strings.Contains(lower, "password") {
t.Fatalf("response body must not contain any password-related key: %s", rec.Body.String())
}
if strings.Contains(rec.Body.String(), "supersecretpw") {
t.Fatal("response body must not contain the plaintext password")
}
})
}
func TestLogin(t *testing.T) {
h, us, _ := newTestAuthHandler()
ctx := context.Background()
hash, err := auth.HashPassword("correcthorse")
if err != nil {
t.Fatalf("failed to hash seed password: %v", err)
}
if err := us.Create(ctx, &models.User{ID: "user-1", Username: "eve", PasswordHash: hash}); err != nil {
t.Fatalf("failed to seed user: %v", err)
}
var wrongPasswordBody, unknownUserBody string
t.Run("success returns 200 with non-empty session_token", func(t *testing.T) {
rec := doJSONRequest(h.Login, http.MethodPost, "/login", map[string]string{"username": "eve", "password": "correcthorse"})
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
assertJSONContentType(t, rec)
var body map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("expected JSON body: %v", err)
}
tok, _ := body["session_token"].(string)
if tok == "" {
t.Fatal("expected non-empty session_token")
}
})
t.Run("wrong password returns 401", func(t *testing.T) {
rec := doJSONRequest(h.Login, http.MethodPost, "/login", map[string]string{"username": "eve", "password": "wrongpass"})
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
assertJSONContentType(t, rec)
wrongPasswordBody = rec.Body.String()
})
t.Run("unknown username returns 401", func(t *testing.T) {
rec := doJSONRequest(h.Login, http.MethodPost, "/login", map[string]string{"username": "ghost", "password": "whatever"})
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
assertJSONContentType(t, rec)
unknownUserBody = rec.Body.String()
})
t.Run("401 bodies for wrong password and unknown user are byte-for-byte identical", func(t *testing.T) {
if wrongPasswordBody == "" || unknownUserBody == "" {
t.Fatal("prior subtests did not populate response bodies")
}
if wrongPasswordBody != unknownUserBody {
t.Fatalf("expected identical bodies, got %q vs %q", wrongPasswordBody, unknownUserBody)
}
})
t.Run("malformed JSON returns 400", func(t *testing.T) {
rec := doJSONRequest(h.Login, http.MethodPost, "/login", "{bad json")
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
}
func TestLogout(t *testing.T) {
h, _, ss := newTestAuthHandler()
ctx := context.Background()
validToken := "validtoken1234567890123456789012"
if err := ss.Create(ctx, &models.Session{Token: validToken, UserID: "user-1"}); err != nil {
t.Fatalf("failed to seed session: %v", err)
}
t.Run("valid token returns 204 with empty body and removes the session", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
req.Header.Set("Authorization", "Bearer "+validToken)
rec := httptest.NewRecorder()
h.Logout(rec, req)
if rec.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d: %s", rec.Code, rec.Body.String())
}
// 204 is exempt from the Content-Type:application/json requirement.
if rec.Body.Len() != 0 {
t.Fatalf("expected empty body, got %q", rec.Body.String())
}
if ss.has(validToken) {
t.Fatal("expected session to be removed from the fake store")
}
})
t.Run("missing token returns 401", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
rec := httptest.NewRecorder()
h.Logout(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("unknown token returns 401", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
req.Header.Set("Authorization", "Bearer not-a-real-token-000000000000")
rec := httptest.NewRecorder()
h.Logout(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
t.Run("second logout with the same token returns 401", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
req.Header.Set("Authorization", "Bearer "+validToken)
rec := httptest.NewRecorder()
h.Logout(rec, req)
if rec.Code != http.StatusUnauthorized {
t.Fatalf("expected 401 on second logout, got %d", rec.Code)
}
assertJSONContentType(t, rec)
})
}