diff --git a/server/internal/handlers/admin_handlers.go b/server/internal/handlers/admin_handlers.go index 0ae33e2..02b436c 100644 --- a/server/internal/handlers/admin_handlers.go +++ b/server/internal/handlers/admin_handlers.go @@ -15,14 +15,16 @@ import ( "golang.org/x/crypto/bcrypt" ) -type createUserRequest struct { +// CreateUserRequest holds the request fields for creating a new user +type CreateUserRequest struct { Email string `json:"email"` DisplayName string `json:"displayName"` Password string `json:"password"` Role models.UserRole `json:"role"` } -type updateUserRequest struct { +// UpdateUserRequest holds the request fields for updating a user +type UpdateUserRequest struct { Email string `json:"email,omitempty"` DisplayName string `json:"displayName,omitempty"` Password string `json:"password,omitempty"` @@ -45,7 +47,7 @@ func (h *Handler) AdminListUsers() http.HandlerFunc { // AdminCreateUser creates a new user func (h *Handler) AdminCreateUser() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - var req createUserRequest + var req CreateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return @@ -136,7 +138,7 @@ func (h *Handler) AdminUpdateUser() http.HandlerFunc { return } - var req updateUserRequest + var req UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Invalid request body", http.StatusBadRequest) return diff --git a/server/internal/handlers/admin_handlers_integration_test.go b/server/internal/handlers/admin_handlers_integration_test.go new file mode 100644 index 0000000..121ea4b --- /dev/null +++ b/server/internal/handlers/admin_handlers_integration_test.go @@ -0,0 +1,243 @@ +//go:build integration + +package handlers_test + +import ( + "encoding/json" + "fmt" + "net/http" + "testing" + + "novamd/internal/handlers" + "novamd/internal/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Helper function to check if a user exists in a slice of users +func containsUser(users []*models.User, searchUser *models.User) bool { + for _, u := range users { + if u.ID == searchUser.ID && + u.Email == searchUser.Email && + u.DisplayName == searchUser.DisplayName && + u.Role == searchUser.Role { + return true + } + } + return false +} + +func TestAdminHandlers_Integration(t *testing.T) { + h := setupTestHarness(t) + defer h.teardown(t) + + t.Run("user management", func(t *testing.T) { + t.Run("list users", func(t *testing.T) { + // Test with admin token + rr := h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var users []*models.User + err := json.NewDecoder(rr.Body).Decode(&users) + require.NoError(t, err) + + // Should have at least our admin and regular test users + assert.GreaterOrEqual(t, len(users), 2) + assert.True(t, containsUser(users, h.AdminUser), "Admin user not found in users list") + assert.True(t, containsUser(users, h.RegularUser), "Regular user not found in users list") + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + + // Test without token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users", nil, "", nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + + t.Run("create user", func(t *testing.T) { + createReq := handlers.CreateUserRequest{ + Email: "newuser@test.com", + DisplayName: "New User", + Password: "password123", + Role: models.RoleEditor, + } + + // Test with admin token + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var createdUser models.User + err := json.NewDecoder(rr.Body).Decode(&createdUser) + require.NoError(t, err) + assert.Equal(t, createReq.Email, createdUser.Email) + assert.Equal(t, createReq.DisplayName, createdUser.DisplayName) + assert.Equal(t, createReq.Role, createdUser.Role) + assert.NotZero(t, createdUser.LastWorkspaceID) + + // Test duplicate email + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + assert.Equal(t, http.StatusConflict, rr.Code) + + // Test invalid request (missing required fields) + invalidReq := handlers.CreateUserRequest{ + Email: "invalid@test.com", + // Missing password and role + } + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", invalidReq, h.AdminToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + + t.Run("get user", func(t *testing.T) { + path := fmt.Sprintf("/api/v1/admin/users/%d", h.RegularUser.ID) + + // Test with admin token + rr := h.makeRequest(t, http.MethodGet, path, nil, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var user models.User + err := json.NewDecoder(rr.Body).Decode(&user) + require.NoError(t, err) + assert.Equal(t, h.RegularUser.ID, user.ID) + + // Test non-existent user + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/users/999999", nil, h.AdminToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodGet, path, nil, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + + t.Run("update user", func(t *testing.T) { + path := fmt.Sprintf("/api/v1/admin/users/%d", h.RegularUser.ID) + updateReq := handlers.UpdateUserRequest{ + DisplayName: "Updated Name", + Role: models.RoleViewer, + } + + // Test with admin token + rr := h.makeRequest(t, http.MethodPut, path, updateReq, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var updatedUser models.User + err := json.NewDecoder(rr.Body).Decode(&updatedUser) + require.NoError(t, err) + assert.Equal(t, updateReq.DisplayName, updatedUser.DisplayName) + assert.Equal(t, updateReq.Role, updatedUser.Role) + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodPut, path, updateReq, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + + t.Run("delete user", func(t *testing.T) { + // Create a user to delete + createReq := handlers.CreateUserRequest{ + Email: "todelete@test.com", + DisplayName: "To Delete", + Password: "password123", + Role: models.RoleEditor, + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/admin/users", createReq, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var createdUser models.User + err := json.NewDecoder(rr.Body).Decode(&createdUser) + require.NoError(t, err) + + path := fmt.Sprintf("/api/v1/admin/users/%d", createdUser.ID) + + // Test deleting own account (should fail) + adminPath := fmt.Sprintf("/api/v1/admin/users/%d", h.AdminUser.ID) + rr = h.makeRequest(t, http.MethodDelete, adminPath, nil, h.AdminToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + + // Test with admin token + rr = h.makeRequest(t, http.MethodDelete, path, nil, h.AdminToken, nil) + assert.Equal(t, http.StatusNoContent, rr.Code) + + // Verify user is deleted + rr = h.makeRequest(t, http.MethodGet, path, nil, h.AdminToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodDelete, path, nil, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + }) + + t.Run("workspace management", func(t *testing.T) { + t.Run("list workspaces", func(t *testing.T) { + // Create a test workspace first + workspace := &models.Workspace{ + UserID: h.RegularUser.ID, + Name: "Test Workspace", + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Test with admin token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var workspaces []*handlers.WorkspaceStats + err := json.NewDecoder(rr.Body).Decode(&workspaces) + require.NoError(t, err) + + // Should have at least the default workspaces for admin and regular users + assert.NotEmpty(t, workspaces) + + // Verify workspace stats fields + for _, ws := range workspaces { + assert.NotZero(t, ws.UserID) + assert.NotEmpty(t, ws.UserEmail) + assert.NotZero(t, ws.WorkspaceID) + assert.NotEmpty(t, ws.WorkspaceName) + assert.NotZero(t, ws.WorkspaceCreatedAt) + assert.GreaterOrEqual(t, ws.TotalFiles, 0) + assert.GreaterOrEqual(t, ws.TotalSize, int64(0)) + } + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/workspaces", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) + }) + + t.Run("system stats", func(t *testing.T) { + // Create some test data + workspace := &models.Workspace{ + UserID: h.RegularUser.ID, + Name: "Stats Test Workspace", + } + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Test with admin token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.AdminToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var stats handlers.SystemStats + err := json.NewDecoder(rr.Body).Decode(&stats) + require.NoError(t, err) + + // Verify stats fields + assert.GreaterOrEqual(t, stats.TotalUsers, 2) // At least admin and regular user + assert.GreaterOrEqual(t, stats.TotalWorkspaces, 2) // At least default workspaces + assert.GreaterOrEqual(t, stats.ActiveUsers, 2) // Our test users should be active + assert.GreaterOrEqual(t, stats.TotalFiles, 0) + assert.GreaterOrEqual(t, stats.TotalSize, int64(0)) + + // Test with non-admin token + rr = h.makeRequest(t, http.MethodGet, "/api/v1/admin/stats", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusForbidden, rr.Code) + }) +} diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 4201fdd..ea26b90 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -91,23 +91,26 @@ func setupTestHarness(t *testing.T) *testHarness { api.SetupRoutes(r, database, storageSvc, authMiddleware, sessionSvc) }) - // Create test users - adminUser, adminToken := createTestUser(t, database, sessionSvc, "admin@test.com", "admin123", models.RoleAdmin) - regularUser, regularToken := createTestUser(t, database, sessionSvc, "user@test.com", "user123", models.RoleEditor) - - return &testHarness{ + h := &testHarness{ DB: database, Storage: storageSvc, Router: router, Handler: handler, JWTManager: jwtSvc, SessionSvc: sessionSvc, - AdminUser: adminUser, - AdminToken: adminToken, - RegularUser: regularUser, - RegularToken: regularToken, TempDirectory: tempDir, } + + // Create test users + adminUser, adminToken := h.createTestUser(t, database, sessionSvc, "admin@test.com", "admin123", models.RoleAdmin) + regularUser, regularToken := h.createTestUser(t, database, sessionSvc, "user@test.com", "user123", models.RoleEditor) + + h.AdminUser = adminUser + h.AdminToken = adminToken + h.RegularUser = regularUser + h.RegularToken = regularToken + + return h } // teardownTestHarness cleans up the test environment @@ -124,7 +127,7 @@ func (h *testHarness) teardown(t *testing.T) { } // createTestUser creates a test user and returns the user and access token -func createTestUser(t *testing.T, db db.Database, sessionSvc *auth.SessionService, email, password string, role models.UserRole) (*models.User, string) { +func (h *testHarness) createTestUser(t *testing.T, db db.Database, sessionSvc *auth.SessionService, email, password string, role models.UserRole) (*models.User, string) { t.Helper() hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) @@ -144,6 +147,12 @@ func createTestUser(t *testing.T, db db.Database, sessionSvc *auth.SessionServic t.Fatalf("Failed to create user: %v", err) } + // Initialize the default workspace directory in storage + err = h.Storage.InitializeUserWorkspace(user.ID, user.LastWorkspaceID) + if err != nil { + t.Fatalf("Failed to initialize user workspace: %v", err) + } + session, accessToken, err := sessionSvc.CreateSession(user.ID, string(user.Role)) if err != nil { t.Fatalf("Failed to create session: %v", err)