From ae48761d349e50331f51b4692a004c211cb9376e Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sat, 30 Nov 2024 11:44:17 +0100 Subject: [PATCH] Implement workspace handlers integration tests --- .../internal/handlers/workspace_handlers.go | 5 + .../workspace_handlers_integration_test.go | 297 ++++++++++++++++++ server/internal/models/workspace.go | 5 + 3 files changed, 307 insertions(+) create mode 100644 server/internal/handlers/workspace_handlers_integration_test.go diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index 06a2503..dc4d94d 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -41,6 +41,11 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { return } + if err := workspace.ValidateGitSettings(); err != nil { + http.Error(w, "Invalid workspace", http.StatusBadRequest) + return + } + workspace.UserID = ctx.UserID if err := h.DB.CreateWorkspace(&workspace); err != nil { http.Error(w, "Failed to create workspace", http.StatusInternalServerError) diff --git a/server/internal/handlers/workspace_handlers_integration_test.go b/server/internal/handlers/workspace_handlers_integration_test.go new file mode 100644 index 0000000..724350c --- /dev/null +++ b/server/internal/handlers/workspace_handlers_integration_test.go @@ -0,0 +1,297 @@ +//go:build integration + +package handlers_test + +import ( + "encoding/json" + "net/http" + "net/url" + "testing" + + "novamd/internal/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWorkspaceHandlers_Integration(t *testing.T) { + h := setupTestHarness(t) + defer h.teardown(t) + + t.Run("list workspaces", func(t *testing.T) { + t.Run("successful list", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var workspaces []*models.Workspace + err := json.NewDecoder(rr.Body).Decode(&workspaces) + require.NoError(t, err) + assert.NotEmpty(t, workspaces, "User should have at least one default workspace") + }) + + t.Run("unauthorized", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, "", nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + }) + }) + + t.Run("create workspace", func(t *testing.T) { + t.Run("successful create", func(t *testing.T) { + workspace := &models.Workspace{ + Name: "Test Workspace", + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var created models.Workspace + err := json.NewDecoder(rr.Body).Decode(&created) + require.NoError(t, err) + assert.Equal(t, workspace.Name, created.Name) + assert.Equal(t, h.RegularUser.ID, created.UserID) + assert.NotZero(t, created.ID) + }) + + t.Run("create with git settings", func(t *testing.T) { + workspace := &models.Workspace{ + Name: "Git Workspace", + GitEnabled: true, + GitURL: "https://github.com/test/repo.git", + GitUser: "testuser", + GitToken: "testtoken", + GitAutoCommit: true, + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var created models.Workspace + err := json.NewDecoder(rr.Body).Decode(&created) + require.NoError(t, err) + assert.Equal(t, workspace.GitEnabled, created.GitEnabled) + assert.Equal(t, workspace.GitURL, created.GitURL) + assert.Equal(t, workspace.GitUser, created.GitUser) + assert.Equal(t, workspace.GitToken, created.GitToken) + assert.Equal(t, workspace.GitAutoCommit, created.GitAutoCommit) + }) + + t.Run("invalid workspace", func(t *testing.T) { + workspace := &models.Workspace{ + Name: "", // Empty name + GitEnabled: true, + // Missing required Git settings + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + }) + + // Create a workspace for the remaining tests + workspace := &models.Workspace{ + Name: "Test Workspace Operations", + } + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + err := json.NewDecoder(rr.Body).Decode(workspace) + require.NoError(t, err) + + escapedName := url.PathEscape(workspace.Name) + baseURL := "/api/v1/workspaces/" + escapedName + + t.Run("get workspace", func(t *testing.T) { + t.Run("successful get", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var got models.Workspace + err := json.NewDecoder(rr.Body).Decode(&got) + require.NoError(t, err) + assert.Equal(t, workspace.ID, got.ID) + assert.Equal(t, workspace.Name, got.Name) + }) + + t.Run("nonexistent workspace", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/nonexistent", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + + t.Run("unauthorized access", func(t *testing.T) { + // Try accessing with another user's token + rr := h.makeRequest(t, http.MethodGet, baseURL, nil, h.AdminToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + }) + + t.Run("update workspace", func(t *testing.T) { + t.Run("update name", func(t *testing.T) { + workspace.Name = "Updated Workspace" + + rr := h.makeRequest(t, http.MethodPut, baseURL, workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var updated models.Workspace + err := json.NewDecoder(rr.Body).Decode(&updated) + require.NoError(t, err) + assert.Equal(t, workspace.Name, updated.Name) + + // Update baseURL for remaining tests + escapedName = url.PathEscape(workspace.Name) + baseURL = "/api/v1/workspaces/" + escapedName + }) + + t.Run("update settings", func(t *testing.T) { + update := &models.Workspace{ + Name: workspace.Name, + Theme: "dark", + AutoSave: true, + ShowHiddenFiles: true, + } + + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var updated models.Workspace + err := json.NewDecoder(rr.Body).Decode(&updated) + require.NoError(t, err) + assert.Equal(t, update.Theme, updated.Theme) + assert.Equal(t, update.AutoSave, updated.AutoSave) + assert.Equal(t, update.ShowHiddenFiles, updated.ShowHiddenFiles) + }) + + t.Run("enable git", func(t *testing.T) { + update := &models.Workspace{ + Name: workspace.Name, + Theme: "dark", + GitEnabled: true, + GitURL: "https://github.com/test/repo.git", + GitUser: "testuser", + GitToken: "testtoken", + GitAutoCommit: true, + } + + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var updated models.Workspace + err := json.NewDecoder(rr.Body).Decode(&updated) + require.NoError(t, err) + assert.Equal(t, update.GitEnabled, updated.GitEnabled) + assert.Equal(t, update.GitURL, updated.GitURL) + assert.Equal(t, update.GitUser, updated.GitUser) + assert.Equal(t, update.GitToken, updated.GitToken) + + // Mock should have been called to setup git + assert.True(t, h.MockGit.IsInitialized()) + }) + + t.Run("invalid git settings", func(t *testing.T) { + update := &models.Workspace{ + Name: workspace.Name, + GitEnabled: true, + // Missing required Git settings + } + + rr := h.makeRequest(t, http.MethodPut, baseURL, update, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + }) + + t.Run("last workspace", func(t *testing.T) { + t.Run("get last workspace", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var response struct { + LastWorkspaceName string `json:"lastWorkspaceName"` + } + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.NotEmpty(t, response.LastWorkspaceName) + }) + + t.Run("update last workspace", func(t *testing.T) { + req := struct { + WorkspaceName string `json:"workspaceName"` + }{ + WorkspaceName: workspace.Name, + } + + rr := h.makeRequest(t, http.MethodPut, "/api/v1/workspaces/last", req, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Verify the update + rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/last", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var response struct { + LastWorkspaceName string `json:"lastWorkspaceName"` + } + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Equal(t, workspace.Name, response.LastWorkspaceName) + }) + }) + + t.Run("delete workspace", func(t *testing.T) { + // Get current workspaces to know how many we have + rr := h.makeRequest(t, http.MethodGet, "/api/v1/workspaces", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var existingWorkspaces []*models.Workspace + err := json.NewDecoder(rr.Body).Decode(&existingWorkspaces) + require.NoError(t, err) + + // Create a new workspace we can safely delete + newWorkspace := &models.Workspace{ + Name: "Workspace To Delete", + } + rr = h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", newWorkspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + err = json.NewDecoder(rr.Body).Decode(newWorkspace) + require.NoError(t, err) + + t.Run("successful delete", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var response struct { + NextWorkspaceName string `json:"nextWorkspaceName"` + } + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.NotEmpty(t, response.NextWorkspaceName) + + // Verify workspace is deleted + rr = h.makeRequest(t, http.MethodGet, "/api/v1/workspaces/"+url.PathEscape(newWorkspace.Name), nil, h.RegularToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + + t.Run("prevent deleting last workspace", func(t *testing.T) { + // Delete all but one workspace + for i := 0; i < len(existingWorkspaces)-1; i++ { + ws := existingWorkspaces[i] + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(ws.Name), nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + } + + // Try to delete the last remaining workspace + lastWs := existingWorkspaces[len(existingWorkspaces)-1] + rr := h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(lastWs.Name), nil, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + }) + + t.Run("unauthorized deletion", func(t *testing.T) { + // Create a workspace to attempt unauthorized deletion + workspace := &models.Workspace{ + Name: "Unauthorized Delete Test", + } + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", workspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + // Try to delete with wrong user's token + rr = h.makeRequest(t, http.MethodDelete, "/api/v1/workspaces/"+url.PathEscape(workspace.Name), nil, h.AdminToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + }) +} diff --git a/server/internal/models/workspace.go b/server/internal/models/workspace.go index 5a0b7eb..584155a 100644 --- a/server/internal/models/workspace.go +++ b/server/internal/models/workspace.go @@ -29,6 +29,11 @@ func (w *Workspace) Validate() error { return validate.Struct(w) } +// ValidateGitSettings validates the git settings if git is enabled +func (w *Workspace) ValidateGitSettings() error { + return validate.StructExcept(w, "ID", "UserID", "Theme") +} + // SetDefaultSettings sets the default settings for the workspace func (w *Workspace) SetDefaultSettings() {