From 1ddf93a8bec9e95c8507d7edd27622b751e08d36 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Fri, 29 Nov 2024 23:14:36 +0100 Subject: [PATCH] Implement git handlers integration test --- .../handlers/git_handlers_integration_test.go | 181 ++++++++++++++++++ server/internal/handlers/integration_test.go | 15 +- server/internal/handlers/mock_git_test.go | 123 ++++++++++++ .../internal/handlers/workspace_handlers.go | 13 ++ server/internal/models/workspace.go | 23 ++- server/internal/storage/service.go | 8 + 6 files changed, 352 insertions(+), 11 deletions(-) create mode 100644 server/internal/handlers/git_handlers_integration_test.go create mode 100644 server/internal/handlers/mock_git_test.go diff --git a/server/internal/handlers/git_handlers_integration_test.go b/server/internal/handlers/git_handlers_integration_test.go new file mode 100644 index 0000000..6d26039 --- /dev/null +++ b/server/internal/handlers/git_handlers_integration_test.go @@ -0,0 +1,181 @@ +//go:build integration + +package handlers_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "novamd/internal/models" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGitHandlers_Integration(t *testing.T) { + h := setupTestHarness(t) + defer h.teardown(t) + + t.Run("git operations", func(t *testing.T) { + // Setup: Create a workspace with Git enabled + workspace := &models.Workspace{ + UserID: h.RegularUser.ID, + Name: "Git Test Workspace", + GitEnabled: true, + GitURL: "https://github.com/test/repo.git", + GitUser: "testuser", + GitToken: "testtoken", + GitAutoCommit: true, + GitCommitMsgTemplate: "Update: {{message}}", + } + + 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) + + // Construct base URL for Git operations + baseURL := "/api/v1/workspaces/" + url.PathEscape(workspace.Name) + "/git" + + t.Run("stage, commit and push", func(t *testing.T) { + h.MockGit.Reset() + + t.Run("successful commit", func(t *testing.T) { + commitMsg := "Test commit message" + requestBody := map[string]string{ + "message": commitMsg, + } + + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var response map[string]string + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Contains(t, response["message"], "successfully") + + // Verify mock was called correctly + assert.Equal(t, 1, h.MockGit.GetCommitCount(), "Commit should be called once") + assert.Equal(t, 1, h.MockGit.GetPushCount(), "Push should be called once") + assert.Equal(t, commitMsg, h.MockGit.GetLastCommitMessage(), "Commit message should match") + }) + + t.Run("empty commit message", func(t *testing.T) { + h.MockGit.Reset() + requestBody := map[string]string{ + "message": "", + } + + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Equal(t, 0, h.MockGit.GetCommitCount(), "Commit should not be called") + }) + + t.Run("git error", func(t *testing.T) { + h.MockGit.Reset() + h.MockGit.SetError(fmt.Errorf("mock git error")) + + requestBody := map[string]string{ + "message": "Test message", + } + + rr := h.makeRequest(t, http.MethodPost, baseURL+"/commit", requestBody, h.RegularToken, nil) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + h.MockGit.SetError(nil) // Reset error state + }) + }) + + t.Run("pull changes", func(t *testing.T) { + h.MockGit.Reset() + + t.Run("successful pull", func(t *testing.T) { + rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + var response map[string]string + err := json.NewDecoder(rr.Body).Decode(&response) + require.NoError(t, err) + assert.Contains(t, response["message"], "Pulled changes") + + assert.Equal(t, 1, h.MockGit.GetPullCount(), "Pull should be called once") + }) + + t.Run("git error", func(t *testing.T) { + h.MockGit.Reset() + h.MockGit.SetError(fmt.Errorf("mock git error")) + + rr := h.makeRequest(t, http.MethodPost, baseURL+"/pull", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + h.MockGit.SetError(nil) // Reset error state + }) + }) + + t.Run("unauthorized access", func(t *testing.T) { + h.MockGit.Reset() + + tests := []struct { + name string + method string + path string + body interface{} + }{ + { + name: "commit without token", + method: http.MethodPost, + path: baseURL + "/commit", + body: map[string]string{"message": "test"}, + }, + { + name: "pull without token", + method: http.MethodPost, + path: baseURL + "/pull", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Test without token + rr := h.makeRequest(t, tc.method, tc.path, tc.body, "", nil) + assert.Equal(t, http.StatusUnauthorized, rr.Code) + + // Test with wrong user's token + rr = h.makeRequest(t, tc.method, tc.path, tc.body, h.AdminToken, nil) + assert.Equal(t, http.StatusNotFound, rr.Code) + }) + } + }) + + t.Run("workspace without git", func(t *testing.T) { + h.MockGit.Reset() + + // Create a workspace without Git enabled + nonGitWorkspace := &models.Workspace{ + UserID: h.RegularUser.ID, + Name: "Non-Git Workspace", + } + + rr := h.makeRequest(t, http.MethodPost, "/api/v1/workspaces", nonGitWorkspace, h.RegularToken, nil) + require.Equal(t, http.StatusOK, rr.Code) + + err := json.NewDecoder(rr.Body).Decode(nonGitWorkspace) + require.NoError(t, err) + + nonGitBaseURL := "/api/v1/workspaces/" + url.PathEscape(nonGitWorkspace.Name) + "/git" + + // Try to commit + commitMsg := map[string]string{"message": "test"} + rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/commit", commitMsg, h.RegularToken, nil) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + + // Try to pull + rr = h.makeRequest(t, http.MethodPost, nonGitBaseURL+"/pull", nil, h.RegularToken, nil) + assert.Equal(t, http.StatusInternalServerError, rr.Code) + }) + }) +} diff --git a/server/internal/handlers/integration_test.go b/server/internal/handlers/integration_test.go index 087601a..5c65f16 100644 --- a/server/internal/handlers/integration_test.go +++ b/server/internal/handlers/integration_test.go @@ -17,6 +17,7 @@ import ( "novamd/internal/api" "novamd/internal/auth" "novamd/internal/db" + "novamd/internal/git" "novamd/internal/handlers" "novamd/internal/models" "novamd/internal/secrets" @@ -36,6 +37,7 @@ type testHarness struct { RegularUser *models.User RegularToken string TempDirectory string + MockGit *MockGitClient } // setupTestHarness creates a new test environment @@ -63,8 +65,16 @@ func setupTestHarness(t *testing.T) *testHarness { t.Fatalf("Failed to run migrations: %v", err) } - // Initialize storage - storageSvc := storage.NewService(tempDir) + // Create mock git client + mockGit := NewMockGitClient(false) + + // Create storage with mock git client + storageOpts := storage.Options{ + NewGitClient: func(url, user, token, path string) git.Client { + return mockGit + }, + } + storageSvc := storage.NewServiceWithOptions(tempDir, storageOpts) // Initialize JWT service jwtSvc, err := auth.NewJWTService(auth.JWTConfig{ @@ -100,6 +110,7 @@ func setupTestHarness(t *testing.T) *testHarness { JWTManager: jwtSvc, SessionSvc: sessionSvc, TempDirectory: tempDir, + MockGit: mockGit, } // Create test users diff --git a/server/internal/handlers/mock_git_test.go b/server/internal/handlers/mock_git_test.go new file mode 100644 index 0000000..23259f5 --- /dev/null +++ b/server/internal/handlers/mock_git_test.go @@ -0,0 +1,123 @@ +//go:build integration + +package handlers_test + +import ( + "fmt" +) + +// MockGitClient implements the git.Client interface for testing +type MockGitClient struct { + initialized bool + cloned bool + lastCommitMsg string + error error + + pullCount int + commitCount int + pushCount int + cloneCount int + ensureCount int +} + +// NewMockGitClient creates a new mock git client +func NewMockGitClient(shouldError bool) *MockGitClient { + var err error + if shouldError { + err = fmt.Errorf("mock git error") + } + return &MockGitClient{ + error: err, + } +} + +// Clone implements git.Client +func (m *MockGitClient) Clone() error { + if m.error != nil { + return m.error + } + m.cloneCount++ + m.cloned = true + return nil +} + +// Pull implements git.Client +func (m *MockGitClient) Pull() error { + if m.error != nil { + return m.error + } + m.pullCount++ + return nil +} + +// Commit implements git.Client +func (m *MockGitClient) Commit(message string) error { + if m.error != nil { + return m.error + } + m.commitCount++ + m.lastCommitMsg = message + return nil +} + +// Push implements git.Client +func (m *MockGitClient) Push() error { + if m.error != nil { + return m.error + } + m.pushCount++ + return nil +} + +// EnsureRepo implements git.Client +func (m *MockGitClient) EnsureRepo() error { + if m.error != nil { + return m.error + } + m.ensureCount++ + m.initialized = true + return nil +} + +// Helper methods for tests + +func (m *MockGitClient) GetCommitCount() int { + return m.commitCount +} + +func (m *MockGitClient) GetPushCount() int { + return m.pushCount +} + +func (m *MockGitClient) GetPullCount() int { + return m.pullCount +} + +func (m *MockGitClient) GetLastCommitMessage() string { + return m.lastCommitMsg +} + +func (m *MockGitClient) IsInitialized() bool { + return m.initialized +} + +func (m *MockGitClient) IsCloned() bool { + return m.cloned +} + +// Reset resets all counters and states +func (m *MockGitClient) Reset() { + m.initialized = false + m.cloned = false + m.lastCommitMsg = "" + m.pullCount = 0 + m.commitCount = 0 + m.pushCount = 0 + m.cloneCount = 0 + m.ensureCount = 0 +} + +// SetError sets the error state +func (m *MockGitClient) SetError(err error) { + m.error = err +} diff --git a/server/internal/handlers/workspace_handlers.go b/server/internal/handlers/workspace_handlers.go index 10ae3cf..06a2503 100644 --- a/server/internal/handlers/workspace_handlers.go +++ b/server/internal/handlers/workspace_handlers.go @@ -52,6 +52,19 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { return } + if workspace.GitEnabled { + if err := h.Storage.SetupGitRepo( + ctx.UserID, + workspace.ID, + workspace.GitURL, + workspace.GitUser, + workspace.GitToken, + ); err != nil { + http.Error(w, "Failed to setup git repo: "+err.Error(), http.StatusInternalServerError) + return + } + } + respondJSON(w, workspace) } } diff --git a/server/internal/models/workspace.go b/server/internal/models/workspace.go index 191e6d7..5a0b7eb 100644 --- a/server/internal/models/workspace.go +++ b/server/internal/models/workspace.go @@ -31,13 +31,18 @@ func (w *Workspace) Validate() error { // SetDefaultSettings sets the default settings for the workspace func (w *Workspace) SetDefaultSettings() { - w.Theme = "light" - w.AutoSave = false - w.ShowHiddenFiles = false - w.GitEnabled = false - w.GitURL = "" - w.GitUser = "" - w.GitToken = "" - w.GitAutoCommit = false - w.GitCommitMsgTemplate = "${action} ${filename}" + + if w.Theme == "" { + w.Theme = "light" + } + + w.AutoSave = w.AutoSave || false + w.ShowHiddenFiles = w.ShowHiddenFiles || false + w.GitEnabled = w.GitEnabled || false + + w.GitAutoCommit = w.GitEnabled && (w.GitAutoCommit || false) + + if w.GitCommitMsgTemplate == "" { + w.GitCommitMsgTemplate = "${action} ${filename}" + } } diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 7b8b0f5..0516b5f 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -35,6 +35,14 @@ func NewService(rootDir string) *Service { // NewServiceWithOptions creates a new Storage instance with the given options and the given rootDir root directory. func NewServiceWithOptions(rootDir string, options Options) *Service { + if options.Fs == nil { + options.Fs = &osFS{} + } + + if options.NewGitClient == nil { + options.NewGitClient = git.New + } + return &Service{ fs: options.Fs, newGitClient: options.NewGitClient,