package api import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config" "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" ) // newTestHandler creates an API handler with a nil DB for testing validation // and routing only. Handers that touch the database will panic with nil DB — // integration tests with a real database cover those paths. func newTestHandler(t *testing.T) *Handler { t.Helper() dir := t.TempDir() cfg := &config.Config{ Port: 8080, CASDir: dir, SkinsDir: dir, BaseURL: "https://test.example.com", JWTSecret: "test-secret", } return &Handler{db: &database.DB{}, cfg: cfg} } // TestRegisterValidation tests input validation in the register handler // without requiring a database connection. func TestRegisterValidation(t *testing.T) { h := newTestHandler(t) mux := http.NewServeMux() h.RegisterRoutes(mux) tests := []struct { name string body string wantStatus int wantErr string }{ { name: "empty body", body: "{}", wantStatus: http.StatusBadRequest, wantErr: "Username, email and password are required", }, { name: "invalid email", body: `{"username":"test","email":"notanemail","password":"pass"}`, wantStatus: http.StatusBadRequest, wantErr: "Invalid email address", }, { name: "invalid json", body: "not json", wantStatus: http.StatusBadRequest, wantErr: "Invalid JSON", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("POST", "/api/web/register", bytes.NewReader([]byte(tt.body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("status = %d, want %d", w.Code, tt.wantStatus) } var resp map[string]string json.Unmarshal(w.Body.Bytes(), &resp) if got := resp["error"]; got != tt.wantErr { t.Errorf("error = %q, want %q", got, tt.wantErr) } }) } } // TestWebLoginValidation tests input validation in the web login handler. func TestWebLoginValidation(t *testing.T) { h := newTestHandler(t) mux := http.NewServeMux() h.RegisterRoutes(mux) tests := []struct { name string body string wantStatus int }{ { name: "empty credentials", body: `{"username":"","password":""}`, wantStatus: http.StatusBadRequest, }, { name: "missing username", body: `{"password":"secret"}`, wantStatus: http.StatusBadRequest, }, { name: "missing password", body: `{"username":"test"}`, wantStatus: http.StatusBadRequest, }, { name: "invalid json", body: "not json", wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { req := httptest.NewRequest("POST", "/api/web/login", bytes.NewReader([]byte(tt.body))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("status = %d, want %d", w.Code, tt.wantStatus) } }) } } // TestLauncherLatest_MissingParams tests that missing query parameters // return 400 without hitting the database. func TestLauncherLatest_MissingParams(t *testing.T) { h := newTestHandler(t) mux := http.NewServeMux() h.RegisterRoutes(mux) queries := []string{ "/api/launcher/latest", "/api/launcher/latest?os=windows", "/api/launcher/latest?arch=amd64", } for _, q := range queries { req := httptest.NewRequest("GET", q, nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("%s: expected 400, got %d", q, w.Code) } } } // TestAuthMiddleware_NoToken tests that protected endpoints reject // requests without a Bearer token. func TestAuthMiddleware_NoToken(t *testing.T) { h := newTestHandler(t) mux := http.NewServeMux() h.RegisterRoutes(mux) protected := []struct { method string path string }{ {"POST", "/api/web/profile/skin"}, {"POST", "/api/web/profile/cape"}, {"DELETE", "/api/web/profile/skin"}, {"DELETE", "/api/web/profile/cape"}, } for _, ep := range protected { t.Run(ep.method+" "+ep.path, func(t *testing.T) { req := httptest.NewRequest(ep.method, ep.path, nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("%s %s: expected 401, got %d", ep.method, ep.path, w.Code) } var resp map[string]string json.Unmarshal(w.Body.Bytes(), &resp) if !strings.Contains(resp["error"], "Missing authorization") { t.Errorf("unexpected error: %s", resp["error"]) } }) } } // TestRoutesRegistered verifies all expected API routes are mounted // and return proper HTTP status codes (not 404 for known paths). func TestRoutesRegistered(t *testing.T) { h := newTestHandler(t) mux := http.NewServeMux() h.RegisterRoutes(mux) // Public routes that should respond without a database. // Only routes with early validation (before DB access) are listed. knownRoutes := []struct { method string path string }{ {"POST", "/api/web/register"}, {"POST", "/api/web/login"}, {"GET", "/api/launcher/latest"}, } for _, r := range knownRoutes { t.Run(r.method+" "+r.path, func(t *testing.T) { req := httptest.NewRequest(r.method, r.path, nil) w := httptest.NewRecorder() mux.ServeHTTP(w, req) // Should not be 404 (route exists). if w.Code == http.StatusNotFound { t.Errorf("%s %s: route not found (404)", r.method, r.path) } }) } }