golang-template/internal/handlers/login_test.go
2026-08-15 23:57:52 +03:00

160 lines
4.5 KiB
Go

package handlers
import (
"bytes"
"encoding/base64"
"errors"
"net/http"
"net/http/httptest"
"testing"
"go-backend/internal/auth"
"go-backend/internal/database"
"golang.org/x/crypto/argon2"
)
// fakeUsers and fakeAuth satisfy the UserService and AuthService interfaces
// declared in handler.go. No database, no network, no real hashing work —
// each test controls exactly what the "service layer" returns.
type fakeUsers struct {
user *database.User
err error
}
func (f *fakeUsers) FindByUsername(username string) (*database.User, error) {
return f.user, f.err
}
func (f *fakeUsers) FindByID(id string) (*database.User, error) {
return f.user, f.err
}
type fakeAuth struct {
session *auth.SessionWithToken
err error
}
func (f *fakeAuth) CreateSession(userID string) (*auth.SessionWithToken, error) {
return f.session, f.err
}
func (f *fakeAuth) ValidateSessionToken(token string) (*auth.Session, error) {
return nil, nil
}
// hashPassword builds a real Argon2id hash in the format auth.VerifyPassword
// expects, so the "correct password" test case exercises the real
// verification logic rather than a stub.
func hashPassword(password, salt string) string {
saltBytes := []byte(salt)
hash := argon2.IDKey([]byte(password), saltBytes, 3, 65536, 4, 32)
return "$argon2id$v=19$m=65536,t=3,p=4$" +
base64.RawStdEncoding.EncodeToString(saltBytes) + "$" +
base64.RawStdEncoding.EncodeToString(hash)
}
func TestLoginHandler(t *testing.T) {
validUser := &database.User{ID: "user-1", Username: "alice", PasswordHash: hashPassword("correct-horse", "somesalt16bytes!")}
tests := []struct {
name string
body string
users UserService
auth AuthService
wantStatus int
}{
{
name: "successful login sets session cookie",
body: `{"username":"alice","password":"correct-horse"}`,
users: &fakeUsers{user: validUser},
auth: &fakeAuth{session: &auth.SessionWithToken{Token: "abc.def"}},
wantStatus: http.StatusOK,
},
{
name: "wrong password rejected",
body: `{"username":"alice","password":"wrong-password"}`,
users: &fakeUsers{user: validUser},
auth: &fakeAuth{},
wantStatus: http.StatusBadRequest,
},
{
name: "unknown username rejected",
body: `{"username":"nobody","password":"whatever"}`,
users: &fakeUsers{user: nil},
auth: &fakeAuth{},
wantStatus: http.StatusBadRequest,
},
{
name: "missing fields rejected before hitting services",
body: `{"username":"","password":""}`,
users: &fakeUsers{err: errors.New("should never be called")},
auth: &fakeAuth{},
wantStatus: http.StatusBadRequest,
},
{
name: "user lookup failure returns 500",
body: `{"username":"alice","password":"correct-horse"}`,
users: &fakeUsers{err: errors.New("connection refused")},
auth: &fakeAuth{},
wantStatus: http.StatusInternalServerError,
},
{
name: "session creation failure returns 500",
body: `{"username":"alice","password":"correct-horse"}`,
users: &fakeUsers{user: validUser},
auth: &fakeAuth{err: errors.New("disk full")},
wantStatus: http.StatusInternalServerError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := NewHandler(tt.users, tt.auth)
req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBufferString(tt.body))
w := httptest.NewRecorder()
h.LoginHandler(w, req)
if w.Code != tt.wantStatus {
t.Fatalf("status = %d, want %d (body: %s)", w.Code, tt.wantStatus, w.Body.String())
}
if tt.wantStatus == http.StatusOK {
resp := w.Result()
cookies := resp.Cookies()
if len(cookies) != 1 || cookies[0].Name != "session" || cookies[0].Value != "abc.def" {
t.Fatalf("expected session cookie with token abc.def, got %+v", cookies)
}
}
})
}
}
func TestLoginHandlerRejectsInvalidJSON(t *testing.T) {
h := NewHandler(&fakeUsers{}, &fakeAuth{})
req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBufferString(`not-json`))
w := httptest.NewRecorder()
h.LoginHandler(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
func TestLoginHandlerRejectsWrongMethod(t *testing.T) {
h := NewHandler(&fakeUsers{}, &fakeAuth{})
req := httptest.NewRequest(http.MethodGet, "/login", nil)
w := httptest.NewRecorder()
h.LoginHandler(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Fatalf("status = %d, want %d", w.Code, http.StatusMethodNotAllowed)
}
}