main_test.go
1package main
2
3import (
4 "bufio"
5 "os"
6 "os/exec"
7 "path/filepath"
8 "runtime"
9 "strings"
10 "testing"
11
12 "github.com/git-bug/git-bug/entities/identity"
13 "github.com/git-bug/git-bug/repository"
14)
15
16func TestPromptUser(t *testing.T) {
17 tests := []struct {
18 name string
19 input string
20 prompt string
21 expected string
22 expectError bool
23 }{
24 {
25 name: "simple input",
26 input: "Test User\n",
27 prompt: "Enter name: ",
28 expected: "Test User",
29 },
30 {
31 name: "input with spaces",
32 input: "John Doe\n",
33 prompt: "Enter name: ",
34 expected: "John Doe",
35 },
36 {
37 name: "empty input",
38 input: "\n",
39 prompt: "Enter name: ",
40 expected: "",
41 },
42 {
43 name: "input with trailing newline",
44 input: "[email protected]\n",
45 prompt: "Enter email: ",
46 expected: "[email protected]",
47 },
48 }
49
50 for _, tt := range tests {
51 t.Run(tt.name, func(t *testing.T) {
52 // Create a scanner from the test input string
53 scanner := bufio.NewScanner(strings.NewReader(tt.input))
54
55 // Call promptUserWithScanner with our test scanner
56 result, err := promptUserWithScanner(tt.prompt, scanner)
57
58 if tt.expectError {
59 if err == nil {
60 t.Errorf("expected error, got nil")
61 }
62 return
63 }
64
65 if err != nil {
66 t.Errorf("unexpected error: %v", err)
67 return
68 }
69
70 if result != tt.expected {
71 t.Errorf("promptUser() = %q, want %q", result, tt.expected)
72 }
73 })
74 }
75}
76
77func TestDetectJJConfig(t *testing.T) {
78 // Test case 1: No .jj directory
79 t.Run("no_jj_directory", func(t *testing.T) {
80 tmpDir := t.TempDir()
81 name, email, err := detectJJConfig(tmpDir)
82 if err != nil {
83 t.Fatalf("unexpected error: %v", err)
84 }
85 if name != "" || email != "" {
86 t.Errorf("expected empty name and email, got name=%q, email=%q", name, email)
87 }
88 })
89
90 // Test case 2: .jj directory exists (would need actual jj config in practice)
91 t.Run("jj_directory_exists", func(t *testing.T) {
92 // This test documents the expected behavior when .jj exists
93 // In a real scenario, it would try to run jj commands
94 tmpDir := t.TempDir()
95 jjDir := filepath.Join(tmpDir, ".jj")
96 if err := os.MkdirAll(jjDir, 0755); err != nil {
97 t.Fatalf("failed to create .jj directory: %v", err)
98 }
99 // We expect the function to attempt jj config commands
100 // Since jj is not available in test env, we just verify the directory detection
101 _, _, err := detectJJConfig(tmpDir)
102 // We expect an error since jj binary won't be available
103 if err == nil {
104 t.Skip("jj not available in test environment, skipping")
105 }
106 })
107}
108
109func TestCreateIdentity(t *testing.T) {
110 // Create a temporary directory for a test git repo
111 tmpDir := t.TempDir()
112
113 // Initialize a git repo
114 initCmd := exec.Command("git", "init")
115 initCmd.Dir = tmpDir
116 if err := initCmd.Run(); err != nil {
117 t.Fatalf("failed to init git repo: %v", err)
118 }
119
120 // Test creating an identity
121 err := createIdentity(tmpDir, "Test User", "[email protected]", true)
122 if err != nil {
123 t.Fatalf("createIdentity failed: %v", err)
124 }
125
126 // Verify identity was created by checking if we can get it
127 repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
128 if err != nil {
129 t.Fatalf("failed to open repo: %v", err)
130 }
131 defer repo.Close()
132
133 isSet, err := identity.IsUserIdentitySet(repo)
134 if err != nil {
135 t.Fatalf("IsUserIdentitySet failed: %v", err)
136 }
137 if !isSet {
138 t.Error("expected user identity to be set")
139 }
140
141 userIdentity, err := identity.GetUserIdentity(repo)
142 if err != nil {
143 t.Fatalf("GetUserIdentity failed: %v", err)
144 }
145
146 if userIdentity.Name() != "Test User" {
147 t.Errorf("expected name 'Test User', got %q", userIdentity.Name())
148 }
149
150 if userIdentity.Email() != "[email protected]" {
151 t.Errorf("expected email '[email protected]', got %q", userIdentity.Email())
152 }
153}
154
155func TestInitCommand_InteractiveMode(t *testing.T) {
156 // Create a temporary directory for a test git repo
157 tmpDir := t.TempDir()
158
159 // Initialize a git repo
160 initCmd := exec.Command("git", "init")
161 initCmd.Dir = tmpDir
162 if err := initCmd.Run(); err != nil {
163 t.Fatalf("failed to init git repo: %v", err)
164 }
165
166 // Add origin remote (required for bug init)
167 remoteCmd := exec.Command("git", "remote", "add", "origin", "[email protected]:test/repo.git")
168 remoteCmd.Dir = tmpDir
169 if err := remoteCmd.Run(); err != nil {
170 t.Fatalf("failed to add origin remote: %v", err)
171 }
172
173 // Test the init function with a mock input (simulating interactive mode)
174 // Since we don't have jj, we'll test the interactive path by mocking stdin
175 input := "Interactive User\[email protected]\n"
176 r, w, _ := os.Pipe()
177 go func() {
178 w.WriteString(input)
179 w.Close()
180 }()
181
182 // We need to test runInitWithReader to inject our stdin
183 err := runInitWithReader(tmpDir, r)
184 if err != nil {
185 t.Fatalf("runInitWithReader failed: %v", err)
186 }
187
188 // Verify repo was opened
189 repo, err := repository.OpenGoGitRepo(tmpDir, "", nil)
190 if err != nil {
191 t.Fatalf("failed to open repo: %v", err)
192 }
193 defer repo.Close()
194
195 // Check if identities were created
196 ids, err := identity.ListLocalIds(repo)
197 if err != nil {
198 t.Fatalf("ListLocalIds failed: %v", err)
199 }
200
201 // Should have at least 2 identities (user + agent)
202 if len(ids) < 2 {
203 t.Errorf("expected at least 2 identities (user + agent), got %d", len(ids))
204 }
205
206 // Verify the user identity has the expected name and email
207 userIdentity, err := identity.GetUserIdentity(repo)
208 if err != nil {
209 t.Fatalf("GetUserIdentity failed: %v", err)
210 }
211
212 if userIdentity.Name() != "Interactive User" {
213 t.Errorf("expected user name 'Interactive User', got %q", userIdentity.Name())
214 }
215
216 if userIdentity.Email() != "[email protected]" {
217 t.Errorf("expected user email '[email protected]', got %q", userIdentity.Email())
218 }
219
220 // Verify the agent identity exists with empty email
221 // The agent identity should be one of the identities with name "agent" and empty email
222 foundAgent := false
223 for _, id := range ids {
224 i, err := identity.ReadLocal(repo, id)
225 if err != nil {
226 t.Fatalf("failed to read identity %s: %v", id, err)
227 }
228 if i.Name() == "agent" && i.Email() == "" {
229 foundAgent = true
230 break
231 }
232 }
233 if !foundAgent {
234 t.Error("expected to find an agent identity with empty email")
235 }
236}
237
238func TestParseEditorContent(t *testing.T) {
239 tests := []struct {
240 name string
241 content string
242 expectedTitle string
243 expectedMessage string
244 expectError bool
245 }{
246 {
247 name: "simple title only",
248 content: "Bug title\n",
249 expectedTitle: "Bug title",
250 expectedMessage: "",
251 },
252 {
253 name: "title with description",
254 content: "Bug title\n\nThis is a description\nwith multiple lines\n",
255 expectedTitle: "Bug title",
256 expectedMessage: "\nThis is a description\nwith multiple lines",
257 },
258 {
259 name: "with comment lines",
260 content: ";; This is a comment\nBug title\n\nDescription here\n;; Another comment",
261 expectedTitle: "Bug title",
262 expectedMessage: "Description here",
263 },
264 {
265 name: "empty content",
266 content: "",
267 expectError: true,
268 },
269 {
270 name: "only comments",
271 content: ";; Comment 1\n;; Comment 2\n",
272 expectError: true,
273 },
274 {
275 name: "leading empty lines",
276 content: "\n\n \nActual title\nDescription",
277 expectedTitle: "Actual title",
278 expectedMessage: "Description",
279 },
280 {
281 name: "markdown headers preserved",
282 content: "Bug title\n\n# Header 1\n## Header 2\nContent here",
283 expectedTitle: "Bug title",
284 expectedMessage: "\n# Header 1\n## Header 2\nContent here",
285 },
286 }
287
288 for _, tt := range tests {
289 t.Run(tt.name, func(t *testing.T) {
290 title, message, err := parseEditorContent(tt.content)
291
292 if tt.expectError {
293 if err == nil {
294 t.Errorf("expected error, got nil")
295 }
296 return
297 }
298
299 if err != nil {
300 t.Errorf("unexpected error: %v", err)
301 return
302 }
303
304 if title != tt.expectedTitle {
305 t.Errorf("title = %q, want %q", title, tt.expectedTitle)
306 }
307
308 if message != tt.expectedMessage {
309 t.Errorf("message = %q, want %q", message, tt.expectedMessage)
310 }
311 })
312 }
313}
314
315func TestParseCommentContent(t *testing.T) {
316 tests := []struct {
317 name string
318 content string
319 expectedMessage string
320 expectError bool
321 }{
322 {
323 name: "simple comment",
324 content: "This is a comment\n",
325 expectedMessage: "This is a comment",
326 },
327 {
328 name: "multiline comment",
329 content: "First line\nSecond line\nThird line\n",
330 expectedMessage: "First line\nSecond line\nThird line",
331 },
332 {
333 name: "with comment lines",
334 content: ";; This is a comment\nActual content\n;; Another comment",
335 expectedMessage: "Actual content",
336 },
337 {
338 name: "only comment lines",
339 content: ";; Comment 1\n;; Comment 2\n",
340 expectError: true,
341 },
342 {
343 name: "empty content",
344 content: "",
345 expectError: true,
346 },
347 {
348 name: "only whitespace",
349 content: " \n\n \n",
350 expectError: true,
351 },
352 {
353 name: "leading empty lines",
354 content: "\n\n \nActual content\nMore content",
355 expectedMessage: "Actual content\nMore content",
356 },
357 {
358 name: "trailing empty lines",
359 content: "Actual content\nMore content\n\n \n",
360 expectedMessage: "Actual content\nMore content",
361 },
362 {
363 name: "markdown content",
364 content: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
365 expectedMessage: "# Header\n\n- List item 1\n- List item 2\n\n**Bold text**",
366 },
367 {
368 name: "mixed comments and content",
369 content: ";; Instructions here\n;; More instructions\n\nActual comment\n;; Hidden comment\nMore content\n;; End comment",
370 expectedMessage: "Actual comment\nMore content",
371 },
372 }
373
374 for _, tt := range tests {
375 t.Run(tt.name, func(t *testing.T) {
376 message, err := parseCommentContent(tt.content)
377
378 if tt.expectError {
379 if err == nil {
380 t.Errorf("expected error, got nil")
381 }
382 return
383 }
384
385 if err != nil {
386 t.Errorf("unexpected error: %v", err)
387 return
388 }
389
390 if message != tt.expectedMessage {
391 t.Errorf("message = %q, want %q", message, tt.expectedMessage)
392 }
393 })
394 }
395}
396
397func TestGetEditor(t *testing.T) {
398 // Save original EDITOR value
399 originalEditor := os.Getenv("EDITOR")
400 defer os.Setenv("EDITOR", originalEditor)
401
402 tests := []struct {
403 name string
404 editorEnv string
405 check func(t *testing.T, got string)
406 }{
407 {
408 name: "with EDITOR set",
409 editorEnv: "vim",
410 check: func(t *testing.T, got string) {
411 if !strings.Contains(got, "vim") {
412 t.Errorf("getEditor() = %q, want to contain 'vim'", got)
413 }
414 },
415 },
416 {
417 name: "with EDITOR set to path",
418 editorEnv: "/usr/bin/nano",
419 check: func(t *testing.T, got string) {
420 if !strings.Contains(got, "nano") {
421 t.Errorf("getEditor() = %q, want to contain 'nano'", got)
422 }
423 },
424 },
425 {
426 name: "with EDITOR empty uses default",
427 editorEnv: "",
428 check: func(t *testing.T, got string) {
429 if got == "" {
430 t.Error("expected default editor, got empty string")
431 }
432 // Check it returns expected default based on platform
433 if runtime.GOOS == "windows" {
434 if got != "notepad" {
435 t.Errorf("expected 'notepad' on Windows, got %q", got)
436 }
437 } else {
438 if got != "vi" {
439 t.Errorf("expected 'vi' on Unix, got %q", got)
440 }
441 }
442 },
443 },
444 }
445
446 for _, tt := range tests {
447 t.Run(tt.name, func(t *testing.T) {
448 os.Setenv("EDITOR", tt.editorEnv)
449 got := getEditor()
450 tt.check(t, got)
451 })
452 }
453}