Koito/engine/handlers/handlers_test.go
Ian-J-S ecae3699f1
Engine Handler HTTP Response Tests (#13)
* update handler signatures to use specific store interfaces

* allow skipping Docker-based integration tests for local unit runs
2026-02-26 20:19:48 -08:00

187 lines
5.3 KiB
Go

package handlers
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"golang.org/x/crypto/bcrypt"
"github.com/gabehf/koito/engine/middleware"
"github.com/gabehf/koito/internal/models"
"github.com/gabehf/koito/internal/db"
"github.com/google/uuid"
)
// Focused, small test set that avoids needing a full db.DB mock.
func TestHealthHandler_Returns200(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/health", nil)
HealthHandler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
}
func TestMeHandler_Unauthorized(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/me", nil)
// MeHandler does not use the store for the unauthorized path, pass nil
MeHandler().ServeHTTP(rr, req)
if rr.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", rr.Code)
}
}
func TestMeHandler_Success(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/me", nil)
user := &models.User{ID: 1, Username: "testuser"}
ctx := context.WithValue(req.Context(), middleware.UserContextKey, user)
req = req.WithContext(ctx)
MeHandler().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "testuser") {
t.Fatalf("expected response to contain username, got %s", rr.Body.String())
}
}
func TestGetArtistHandler_MissingID_Returns400(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/artist", nil)
// Handler will validate query param before touching the store; pass nil
http.HandlerFunc(GetArtistHandler(nil)).ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rr.Code)
}
}
func TestGetArtistHandler_InvalidID_Returns400(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/artist?id=abc", nil)
http.HandlerFunc(GetArtistHandler(nil)).ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for invalid id, got %d", rr.Code)
}
}
func TestGetTrackHandler_MissingID_Returns400(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/track", nil)
http.HandlerFunc(GetTrackHandler(nil)).ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected 400, got %d", rr.Code)
}
}
func TestGetTrackHandler_InvalidID_Returns400(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/track?id=xyz", nil)
http.HandlerFunc(GetTrackHandler(nil)).ServeHTTP(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("expected 400 for invalid id, got %d", rr.Code)
}
}
func TestGetArtistHandler_Success(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/artist?id=5", nil)
mock := artistStoreMock{}
http.HandlerFunc(GetArtistHandler(mock)).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Test Artist") {
t.Fatalf("expected body to contain artist name, got %s", rr.Body.String())
}
}
func TestGetTrackHandler_Success(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/track?id=7", nil)
mock := trackStoreMock{}
http.HandlerFunc(GetTrackHandler(mock)).ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "Test Track") {
t.Fatalf("expected body to contain track title, got %s", rr.Body.String())
}
}
func TestLoginHandler_Success(t *testing.T) {
// prepare hashed password
pass := []byte("secretpass")
hashed, _ := bcrypt.GenerateFromPassword(pass, bcrypt.DefaultCost)
store := &loginStoreMock{user: &models.User{ID: 3, Username: "bob", Password: hashed}}
form := "username=bob&password=secretpass"
rr := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
LoginHandler(store).ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("expected 204, got %d", rr.Code)
}
// cookie set
found := false
for _, c := range rr.Result().Cookies() {
if c.Name == "koito_session" && c.Value != "" {
found = true
}
}
if !found {
t.Fatalf("expected koito_session cookie to be set")
}
}
// --- minimal mocks ---
type artistStoreMock struct{}
func (artistStoreMock) GetArtist(ctx context.Context, opts db.GetArtistOpts) (*models.Artist, error) {
return &models.Artist{ID: opts.ID, Name: "Test Artist"}, nil
}
type trackStoreMock struct{}
func (trackStoreMock) GetTrack(ctx context.Context, opts db.GetTrackOpts) (*models.Track, error) {
return &models.Track{ID: opts.ID, Title: "Test Track"}, nil
}
type loginStoreMock struct{ user *models.User }
func (l *loginStoreMock) GetUserByUsername(ctx context.Context, username string) (*models.User, error) {
if l.user != nil && l.user.Username == username {
return l.user, nil
}
return nil, nil
}
func (l *loginStoreMock) SaveSession(ctx context.Context, userId int32, expiresAt time.Time, persistent bool) (*models.Session, error) {
return &models.Session{ID: uuid.New(), UserID: userId, ExpiresAt: expiresAt, Persistent: persistent}, nil
}