diff --git a/server/internal/storage/files_test.go b/server/internal/storage/files_test.go index 55e1f44..d5bf20b 100644 --- a/server/internal/storage/files_test.go +++ b/server/internal/storage/files_test.go @@ -92,7 +92,10 @@ func TestFileNode(t *testing.T) { func TestListFilesRecursively(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) t.Run("empty directory", func(t *testing.T) { mockFS.ReadDirReturns = map[string]struct { @@ -183,7 +186,10 @@ func TestListFilesRecursively(t *testing.T) { func TestGetFileContent(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string @@ -257,7 +263,10 @@ func TestGetFileContent(t *testing.T) { func TestSaveFile(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string @@ -327,7 +336,10 @@ func TestSaveFile(t *testing.T) { func TestDeleteFile(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string diff --git a/server/internal/storage/git.go b/server/internal/storage/git.go index a626c92..09d3b0f 100644 --- a/server/internal/storage/git.go +++ b/server/internal/storage/git.go @@ -27,7 +27,7 @@ func (s *Service) SetupGitRepo(userID, workspaceID int, gitURL, gitUser, gitToke if _, ok := s.GitRepos[userID]; !ok { s.GitRepos[userID] = make(map[int]git.Client) } - s.GitRepos[userID][workspaceID] = git.New(gitURL, gitUser, gitToken, workspacePath) + s.GitRepos[userID][workspaceID] = s.newGitClient(gitURL, gitUser, gitToken, workspacePath) return s.GitRepos[userID][workspaceID].EnsureRepo() } diff --git a/server/internal/storage/git_test.go b/server/internal/storage/git_test.go new file mode 100644 index 0000000..b18ce35 --- /dev/null +++ b/server/internal/storage/git_test.go @@ -0,0 +1,258 @@ +package storage_test + +import ( + "errors" + "testing" + + "novamd/internal/git" + "novamd/internal/storage" +) + +// MockGitClient implements git.Client interface for testing +type MockGitClient struct { + CloneCalled bool + PullCalled bool + CommitCalled bool + PushCalled bool + EnsureCalled bool + CommitMessage string + ReturnError error +} + +func (m *MockGitClient) Clone() error { + m.CloneCalled = true + return m.ReturnError +} + +func (m *MockGitClient) Pull() error { + m.PullCalled = true + return m.ReturnError +} + +func (m *MockGitClient) Commit(message string) error { + m.CommitCalled = true + m.CommitMessage = message + return m.ReturnError +} + +func (m *MockGitClient) Push() error { + m.PushCalled = true + return m.ReturnError +} + +func (m *MockGitClient) EnsureRepo() error { + m.EnsureCalled = true + return m.ReturnError +} + +func TestSetupGitRepo(t *testing.T) { + mockFS := NewMockFS() + + testCases := []struct { + name string + userID int + workspaceID int + gitURL string + gitUser string + gitToken string + mockErr error + wantErr bool + }{ + { + name: "successful setup", + userID: 1, + workspaceID: 1, + gitURL: "https://github.com/user/repo", + gitUser: "user", + gitToken: "token", + mockErr: nil, + wantErr: false, + }, + { + name: "git initialization error", + userID: 1, + workspaceID: 2, + gitURL: "https://github.com/user/repo", + gitUser: "user", + gitToken: "token", + mockErr: errors.New("git initialization failed"), + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a mock client with the desired error behavior + mockClient := &MockGitClient{ReturnError: tc.mockErr} + + // Create a client factory that returns our configured mock + mockClientFactory := func(_, _, _, _ string) git.Client { + return mockClient + } + + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: mockClientFactory, + }) + + // Setup the git repo + err := s.SetupGitRepo(tc.userID, tc.workspaceID, tc.gitURL, tc.gitUser, tc.gitToken) + + if tc.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check if client was stored correctly + client, ok := s.GitRepos[tc.userID][tc.workspaceID] + if !ok { + t.Fatal("git client was not stored in service") + } + + if !mockClient.EnsureCalled { + t.Error("EnsureRepo was not called") + } + + // Verify it's our mock client + if client != mockClient { + t.Error("stored client is not our mock client") + } + }) + } +} + +func TestGitOperations(t *testing.T) { + mockFS := NewMockFS() + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: func(_, _, _, _ string) git.Client { return &MockGitClient{} }, + }) + + t.Run("operations on non-configured workspace", func(t *testing.T) { + err := s.StageCommitAndPush(1, 1, "test commit") + if err == nil { + t.Error("expected error for non-configured workspace, got nil") + } + + err = s.Pull(1, 1) + if err == nil { + t.Error("expected error for non-configured workspace, got nil") + } + }) + + t.Run("successful operations", func(t *testing.T) { + // Initialize GitRepos map + s.GitRepos = make(map[int]map[int]git.Client) + s.GitRepos[1] = make(map[int]git.Client) + mockClient := &MockGitClient{} + s.GitRepos[1][1] = mockClient + + // Test commit and push + err := s.StageCommitAndPush(1, 1, "test commit") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !mockClient.CommitCalled { + t.Error("Commit was not called") + } + if mockClient.CommitMessage != "test commit" { + t.Errorf("Commit message = %q, want %q", mockClient.CommitMessage, "test commit") + } + if !mockClient.PushCalled { + t.Error("Push was not called") + } + + // Test pull + err = s.Pull(1, 1) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !mockClient.PullCalled { + t.Error("Pull was not called") + } + }) + + t.Run("operation errors", func(t *testing.T) { + // Initialize GitRepos map with error-returning client + s.GitRepos = make(map[int]map[int]git.Client) + s.GitRepos[1] = make(map[int]git.Client) + mockClient := &MockGitClient{ReturnError: errors.New("git operation failed")} + s.GitRepos[1][1] = mockClient + + // Test commit error + err := s.StageCommitAndPush(1, 1, "test commit") + if err == nil { + t.Error("expected error for commit, got nil") + } + + // Test pull error + err = s.Pull(1, 1) + if err == nil { + t.Error("expected error for pull, got nil") + } + }) +} + +func TestDisableGitRepo(t *testing.T) { + mockFS := NewMockFS() + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: func(_, _, _, _ string) git.Client { return &MockGitClient{} }, + }) + + testCases := []struct { + name string + userID int + workspaceID int + setupRepo bool + }{ + { + name: "disable existing repo", + userID: 1, + workspaceID: 1, + setupRepo: true, + }, + { + name: "disable non-existent repo", + userID: 2, + workspaceID: 1, + setupRepo: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Reset GitRepos for each test + s.GitRepos = make(map[int]map[int]git.Client) + + if tc.setupRepo { + // Setup initial repo + s.GitRepos[tc.userID] = make(map[int]git.Client) + s.GitRepos[tc.userID][tc.workspaceID] = &MockGitClient{} + } + + // Disable the repo + s.DisableGitRepo(tc.userID, tc.workspaceID) + + // Verify repo was removed + if userRepos, exists := s.GitRepos[tc.userID]; exists { + if _, repoExists := userRepos[tc.workspaceID]; repoExists { + t.Error("git repo still exists after disable") + } + } + + // If this was the user's last repo, verify user entry was cleaned up + if tc.setupRepo { + if len(s.GitRepos[tc.userID]) > 0 { + t.Error("user's git repos map not cleaned up when last repo removed") + } + } + }) + } +} diff --git a/server/internal/storage/service.go b/server/internal/storage/service.go index 985b66f..758e4d2 100644 --- a/server/internal/storage/service.go +++ b/server/internal/storage/service.go @@ -13,9 +13,16 @@ type Manager interface { // Service represents the file system structure. type Service struct { - fs fileSystem - RootDir string - GitRepos map[int]map[int]git.Client // map[userID]map[workspaceID]*git.Client + fs fileSystem + newGitClient func(url, user, token, path string) git.Client + RootDir string + GitRepos map[int]map[int]git.Client // map[userID]map[workspaceID]*git.Client +} + +// Options represents the options for the storage service. +type Options struct { + Fs fileSystem + NewGitClient func(url, user, token, path string) git.Client } // NewService creates a new Storage instance. @@ -24,19 +31,23 @@ type Service struct { // Returns: // - result: the new Storage instance func NewService(rootDir string) *Service { - return NewServiceWithFS(rootDir, &osFS{}) + return NewServiceWithOptions(rootDir, Options{ + Fs: &osFS{}, + NewGitClient: git.New, + }) } -// NewServiceWithFS creates a new Storage instance with the given filesystem. +// NewServiceWithOptions creates a new Storage instance with the given options. // Parameters: // - rootDir: the root directory for the storage -// - fs: the filesystem implementation to use +// - opts: the options for the storage service // Returns: // - result: the new Storage instance -func NewServiceWithFS(rootDir string, fs fileSystem) *Service { +func NewServiceWithOptions(rootDir string, opts Options) *Service { return &Service{ - fs: fs, - RootDir: rootDir, - GitRepos: make(map[int]map[int]git.Client), + fs: opts.Fs, + newGitClient: opts.NewGitClient, + RootDir: rootDir, + GitRepos: make(map[int]map[int]git.Client), } } diff --git a/server/internal/storage/workspace_test.go b/server/internal/storage/workspace_test.go index 752ef18..f6b1607 100644 --- a/server/internal/storage/workspace_test.go +++ b/server/internal/storage/workspace_test.go @@ -11,7 +11,10 @@ import ( func TestValidatePath(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string @@ -94,7 +97,10 @@ func TestValidatePath(t *testing.T) { func TestGetWorkspacePath(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string @@ -134,7 +140,10 @@ func TestGetWorkspacePath(t *testing.T) { func TestInitializeUserWorkspace(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string @@ -199,7 +208,10 @@ func TestInitializeUserWorkspace(t *testing.T) { func TestDeleteUserWorkspace(t *testing.T) { mockFS := NewMockFS() - s := storage.NewServiceWithFS("test-root", mockFS) + s := storage.NewServiceWithOptions("test-root", storage.Options{ + Fs: mockFS, + NewGitClient: nil, + }) testCases := []struct { name string