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) } }