Tuas W06
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user