git_handlers_test.go

  1package server
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"net/http"
  7	"net/http/httptest"
  8	"os"
  9	"os/exec"
 10	"path/filepath"
 11	"testing"
 12)
 13
 14// TestGetGitRoot tests the getGitRoot function
 15func TestGetGitRoot(t *testing.T) {
 16	// Create a temporary directory for testing
 17	tempDir := t.TempDir()
 18
 19	// Test with non-git directory
 20	_, err := getGitRoot(tempDir)
 21	if err == nil {
 22		t.Error("expected error for non-git directory, got nil")
 23	}
 24
 25	// Create a git repository
 26	gitDir := filepath.Join(tempDir, "repo")
 27	err = os.MkdirAll(gitDir, 0o755)
 28	if err != nil {
 29		t.Fatal(err)
 30	}
 31
 32	// Initialize git repo
 33	cmd := exec.Command("git", "init")
 34	cmd.Dir = gitDir
 35	err = cmd.Run()
 36	if err != nil {
 37		t.Fatal(err)
 38	}
 39
 40	// Configure git user for commits
 41	cmd = exec.Command("git", "config", "user.name", "Test User")
 42	cmd.Dir = gitDir
 43	err = cmd.Run()
 44	if err != nil {
 45		t.Fatal(err)
 46	}
 47
 48	cmd = exec.Command("git", "config", "user.email", "test@example.com")
 49	cmd.Dir = gitDir
 50	err = cmd.Run()
 51	if err != nil {
 52		t.Fatal(err)
 53	}
 54
 55	// Test with git directory
 56	root, err := getGitRoot(gitDir)
 57	if err != nil {
 58		t.Errorf("unexpected error for git directory: %v", err)
 59	}
 60	if root != gitDir {
 61		t.Errorf("expected root %s, got %s", gitDir, root)
 62	}
 63
 64	// Test with subdirectory of git directory
 65	subDir := filepath.Join(gitDir, "subdir")
 66	err = os.MkdirAll(subDir, 0o755)
 67	if err != nil {
 68		t.Fatal(err)
 69	}
 70
 71	root, err = getGitRoot(subDir)
 72	if err != nil {
 73		t.Errorf("unexpected error for git subdirectory: %v", err)
 74	}
 75	if root != gitDir {
 76		t.Errorf("expected root %s, got %s", gitDir, root)
 77	}
 78}
 79
 80// TestParseDiffStat tests the parseDiffStat function
 81func TestParseDiffStat(t *testing.T) {
 82	// Test empty output
 83	additions, deletions, filesCount := parseDiffStat("")
 84	if additions != 0 || deletions != 0 || filesCount != 0 {
 85		t.Errorf("expected 0,0,0 for empty output, got %d,%d,%d", additions, deletions, filesCount)
 86	}
 87
 88	// Test single file
 89	output := "5\t3\tfile1.txt\n"
 90	additions, deletions, filesCount = parseDiffStat(output)
 91	if additions != 5 || deletions != 3 || filesCount != 1 {
 92		t.Errorf("expected 5,3,1 for single file, got %d,%d,%d", additions, deletions, filesCount)
 93	}
 94
 95	// Test multiple files
 96	output = "5\t3\tfile1.txt\n10\t2\tfile2.txt\n"
 97	additions, deletions, filesCount = parseDiffStat(output)
 98	if additions != 15 || deletions != 5 || filesCount != 2 {
 99		t.Errorf("expected 15,5,2 for multiple files, got %d,%d,%d", additions, deletions, filesCount)
100	}
101
102	// Test file with additions only
103	output = "5\t0\tfile1.txt\n"
104	additions, deletions, filesCount = parseDiffStat(output)
105	if additions != 5 || deletions != 0 || filesCount != 1 {
106		t.Errorf("expected 5,0,1 for additions only, got %d,%d,%d", additions, deletions, filesCount)
107	}
108
109	// Test file with deletions only
110	output = "0\t3\tfile1.txt\n"
111	additions, deletions, filesCount = parseDiffStat(output)
112	if additions != 0 || deletions != 3 || filesCount != 1 {
113		t.Errorf("expected 0,3,1 for deletions only, got %d,%d,%d", additions, deletions, filesCount)
114	}
115
116	// Test file with binary content (represented as -)
117	output = "-\t-\tfile1.bin\n"
118	additions, deletions, filesCount = parseDiffStat(output)
119	if additions != 0 || deletions != 0 || filesCount != 1 {
120		t.Errorf("expected 0,0,1 for binary file, got %d,%d,%d", additions, deletions, filesCount)
121	}
122}
123
124// setupTestGitRepo creates a temporary git repository with some content for testing
125func setupTestGitRepo(t *testing.T) string {
126	// Create a temporary directory for testing
127	tempDir := t.TempDir()
128
129	// Initialize git repo
130	cmd := exec.Command("git", "init")
131	cmd.Dir = tempDir
132	err := cmd.Run()
133	if err != nil {
134		t.Fatal(err)
135	}
136
137	// Configure git user for commits
138	cmd = exec.Command("git", "config", "user.name", "Test User")
139	cmd.Dir = tempDir
140	err = cmd.Run()
141	if err != nil {
142		t.Fatal(err)
143	}
144
145	cmd = exec.Command("git", "config", "user.email", "test@example.com")
146	cmd.Dir = tempDir
147	err = cmd.Run()
148	if err != nil {
149		t.Fatal(err)
150	}
151
152	// Create and commit a file
153	filePath := filepath.Join(tempDir, "test.txt")
154	content := "Hello, World!\n"
155	err = os.WriteFile(filePath, []byte(content), 0o644)
156	if err != nil {
157		t.Fatal(err)
158	}
159
160	cmd = exec.Command("git", "add", "test.txt")
161	cmd.Dir = tempDir
162	err = cmd.Run()
163	if err != nil {
164		t.Fatal(err)
165	}
166
167	cmd = exec.Command("git", "commit", "-m", "Initial commit\n\nPrompt: Initial test commit for git handlers test", "--author=Test <test@example.com>")
168	cmd.Dir = tempDir
169	output, err := cmd.CombinedOutput()
170	if err != nil {
171		t.Logf("git commit failed: %v", err)
172		t.Logf("git commit output: %s", string(output))
173		t.Fatal(err)
174	}
175
176	// Modify the file (staged changes)
177	newContent := "Hello, World!\nModified content\n"
178	err = os.WriteFile(filePath, []byte(newContent), 0o644)
179	if err != nil {
180		t.Fatal(err)
181	}
182
183	cmd = exec.Command("git", "add", "test.txt")
184	cmd.Dir = tempDir
185	err = cmd.Run()
186	if err != nil {
187		t.Fatal(err)
188	}
189
190	// Modify the file again (unstaged changes)
191	unstagedContent := "Hello, World!\nModified content\nMore changes\n"
192	err = os.WriteFile(filePath, []byte(unstagedContent), 0o644)
193	if err != nil {
194		t.Fatal(err)
195	}
196
197	// Create another file (untracked)
198	untrackedPath := filepath.Join(tempDir, "untracked.txt")
199	untrackedContent := "Untracked file\n"
200	err = os.WriteFile(untrackedPath, []byte(untrackedContent), 0o644)
201	if err != nil {
202		t.Fatal(err)
203	}
204
205	return tempDir
206}
207
208// TestHandleGitDiffs tests the handleGitDiffs function
209func TestHandleGitDiffs(t *testing.T) {
210	h := NewTestHarness(t)
211	defer h.Close()
212
213	// Test with non-git directory
214	req := httptest.NewRequest("GET", "/api/git/diffs?cwd=/tmp", nil)
215	w := httptest.NewRecorder()
216	h.server.handleGitDiffs(w, req)
217
218	if w.Code != http.StatusBadRequest {
219		t.Errorf("expected status 400 for non-git directory, got %d", w.Code)
220	}
221
222	// Setup a test git repository
223	gitDir := setupTestGitRepo(t)
224
225	// Test with valid git directory
226	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/diffs?cwd=%s", gitDir), nil)
227	w = httptest.NewRecorder()
228	h.server.handleGitDiffs(w, req)
229
230	if w.Code != http.StatusOK {
231		t.Errorf("expected status 200 for git directory, got %d: %s", w.Code, w.Body.String())
232	}
233
234	// Check response content type
235	if w.Header().Get("Content-Type") != "application/json" {
236		t.Errorf("expected content-type application/json, got %s", w.Header().Get("Content-Type"))
237	}
238
239	// Parse response
240	var response struct {
241		Diffs   []GitDiffInfo `json:"diffs"`
242		GitRoot string        `json:"gitRoot"`
243	}
244	err := json.Unmarshal(w.Body.Bytes(), &response)
245	if err != nil {
246		t.Fatalf("failed to parse response: %v", err)
247	}
248
249	// Check that we have at least one diff (working changes)
250	if len(response.Diffs) == 0 {
251		t.Error("expected at least one diff (working changes)")
252	}
253
254	// Check that the first diff is working changes
255	if len(response.Diffs) > 0 {
256		diff := response.Diffs[0]
257		if diff.ID != "working" {
258			t.Errorf("expected first diff ID to be 'working', got %s", diff.ID)
259		}
260		if diff.Message != "Working Changes" {
261			t.Errorf("expected first diff message to be 'Working Changes', got %s", diff.Message)
262		}
263	}
264
265	// Check that git root is correct
266	if response.GitRoot != gitDir {
267		t.Errorf("expected git root %s, got %s", gitDir, response.GitRoot)
268	}
269
270	// Test with subdirectory of git directory
271	subDir := filepath.Join(gitDir, "subdir")
272	err = os.MkdirAll(subDir, 0o755)
273	if err != nil {
274		t.Fatal(err)
275	}
276
277	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/diffs?cwd=%s", subDir), nil)
278	w = httptest.NewRecorder()
279	h.server.handleGitDiffs(w, req)
280
281	if w.Code != http.StatusOK {
282		t.Errorf("expected status 200 for git subdirectory, got %d: %s", w.Code, w.Body.String())
283	}
284}
285
286// TestHandleGitDiffFiles tests the handleGitDiffFiles function
287func TestHandleGitDiffFiles(t *testing.T) {
288	h := NewTestHarness(t)
289	defer h.Close()
290
291	// Setup a test git repository
292	gitDir := setupTestGitRepo(t)
293
294	// Test with invalid method
295	req := httptest.NewRequest("POST", fmt.Sprintf("/api/git/diffs/working/files?cwd=%s", gitDir), nil)
296	w := httptest.NewRecorder()
297	h.server.handleGitDiffFiles(w, req)
298
299	if w.Code != http.StatusMethodNotAllowed {
300		t.Errorf("expected status 405 for invalid method, got %d", w.Code)
301	}
302
303	// Test with invalid path
304	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/diffs/working?cwd=%s", gitDir), nil)
305	w = httptest.NewRecorder()
306	h.server.handleGitDiffFiles(w, req)
307
308	if w.Code != http.StatusBadRequest {
309		t.Errorf("expected status 400 for invalid path, got %d", w.Code)
310	}
311
312	// Test with non-git directory
313	req = httptest.NewRequest("GET", "/api/git/diffs/working/files?cwd=/tmp", nil)
314	w = httptest.NewRecorder()
315	h.server.handleGitDiffFiles(w, req)
316
317	if w.Code != http.StatusBadRequest {
318		t.Errorf("expected status 400 for non-git directory, got %d", w.Code)
319	}
320
321	// Test with working changes
322	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/diffs/working/files?cwd=%s", gitDir), nil)
323	w = httptest.NewRecorder()
324	h.server.handleGitDiffFiles(w, req)
325
326	if w.Code != http.StatusOK {
327		t.Errorf("expected status 200 for working changes, got %d: %s", w.Code, w.Body.String())
328	}
329
330	// Check response content type
331	if w.Header().Get("Content-Type") != "application/json" {
332		t.Errorf("expected content-type application/json, got %s", w.Header().Get("Content-Type"))
333	}
334
335	// Parse response
336	var files []GitFileInfo
337	err := json.Unmarshal(w.Body.Bytes(), &files)
338	if err != nil {
339		t.Fatalf("failed to parse response: %v", err)
340	}
341
342	// Check that we have at least one file
343	if len(files) == 0 {
344		t.Error("expected at least one file in working changes")
345	}
346
347	// Check file information
348	if len(files) > 0 {
349		file := files[0]
350		if file.Path != "test.txt" {
351			t.Errorf("expected file path test.txt, got %s", file.Path)
352		}
353		if file.Status != "modified" {
354			t.Errorf("expected file status modified, got %s", file.Status)
355		}
356	}
357}
358
359// TestHandleGitFileDiff tests the handleGitFileDiff function
360func TestHandleGitFileDiff(t *testing.T) {
361	h := NewTestHarness(t)
362	defer h.Close()
363
364	// Setup a test git repository
365	gitDir := setupTestGitRepo(t)
366
367	// Test with invalid method
368	req := httptest.NewRequest("POST", fmt.Sprintf("/api/git/file-diff/working/test.txt?cwd=%s", gitDir), nil)
369	w := httptest.NewRecorder()
370	h.server.handleGitFileDiff(w, req)
371
372	if w.Code != http.StatusMethodNotAllowed {
373		t.Errorf("expected status 405 for invalid method, got %d", w.Code)
374	}
375
376	// Test with invalid path (missing diff ID)
377	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/file-diff/test.txt?cwd=%s", gitDir), nil)
378	w = httptest.NewRecorder()
379	h.server.handleGitFileDiff(w, req)
380
381	if w.Code != http.StatusBadRequest {
382		t.Errorf("expected status 400 for invalid path, got %d", w.Code)
383	}
384
385	// Test with non-git directory
386	req = httptest.NewRequest("GET", "/api/git/file-diff/working/test.txt?cwd=/tmp", nil)
387	w = httptest.NewRecorder()
388	h.server.handleGitFileDiff(w, req)
389
390	if w.Code != http.StatusBadRequest {
391		t.Errorf("expected status 400 for non-git directory, got %d", w.Code)
392	}
393
394	// Test with working changes
395	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/file-diff/working/test.txt?cwd=%s", gitDir), nil)
396	w = httptest.NewRecorder()
397	h.server.handleGitFileDiff(w, req)
398
399	if w.Code != http.StatusOK {
400		t.Errorf("expected status 200 for working changes, got %d: %s", w.Code, w.Body.String())
401	}
402
403	// Check response content type
404	if w.Header().Get("Content-Type") != "application/json" {
405		t.Errorf("expected content-type application/json, got %s", w.Header().Get("Content-Type"))
406	}
407
408	// Parse response
409	var fileDiff GitFileDiff
410	err := json.Unmarshal(w.Body.Bytes(), &fileDiff)
411	if err != nil {
412		t.Fatalf("failed to parse response: %v", err)
413	}
414
415	// Check file information
416	if fileDiff.Path != "test.txt" {
417		t.Errorf("expected file path test.txt, got %s", fileDiff.Path)
418	}
419
420	// Check that we have content
421	if fileDiff.OldContent == "" {
422		t.Error("expected old content")
423	}
424
425	if fileDiff.NewContent == "" {
426		t.Error("expected new content")
427	}
428
429	// Test with path traversal attempt (should be blocked)
430	req = httptest.NewRequest("GET", fmt.Sprintf("/api/git/file-diff/working/../etc/passwd?cwd=%s", gitDir), nil)
431	w = httptest.NewRecorder()
432	h.server.handleGitFileDiff(w, req)
433
434	if w.Code != http.StatusBadRequest {
435		t.Errorf("expected status 400 for path traversal attempt, got %d", w.Code)
436	}
437}