v2.31.0-dev23 58a92f3

service_test.go RawHistoryFolder

1package logs
2
3import (
4	"context"
5	"io"
6	"os"
7	"os/exec"
8	"path/filepath"
9	"testing"
10	"time"
11
12	"github.com/bnema/zerowrap"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16
17	"github.com/bnema/gordon/internal/boundaries/in/mocks"
18	outMocks "github.com/bnema/gordon/internal/boundaries/out/mocks"
19	"github.com/bnema/gordon/internal/domain"
20)
21
22func TestService_GetProcessLogs(t *testing.T) {
23	log := zerowrap.New(zerowrap.Config{Level: "warn"})
24
25	t.Run("returns lines from log file", func(t *testing.T) {
26		// Create temp log file with content
27		tmpDir := t.TempDir()
28		logPath := filepath.Join(tmpDir, "test.log")
29		content := "line1\nline2\nline3\nline4\nline5\n"
30		err := os.WriteFile(logPath, []byte(content), 0644)
31		require.NoError(t, err)
32
33		containerSvc := mocks.NewMockContainerService(t)
34		runtime := outMocks.NewMockContainerRuntime(t)
35
36		svc := NewService(logPath, true, containerSvc, runtime, log)
37
38		lines, err := svc.GetProcessLogs(context.Background(), 3)
39		require.NoError(t, err)
40		assert.Len(t, lines, 3)
41		assert.Equal(t, []string{"line3", "line4", "line5"}, lines)
42	})
43
44	t.Run("returns empty slice for non-existent file", func(t *testing.T) {
45		containerSvc := mocks.NewMockContainerService(t)
46		runtime := outMocks.NewMockContainerRuntime(t)
47
48		svc := NewService("/nonexistent/file.log", true, containerSvc, runtime, log)
49
50		lines, err := svc.GetProcessLogs(context.Background(), 10)
51		require.NoError(t, err)
52		assert.Empty(t, lines)
53	})
54
55	t.Run("returns error when log file path not configured", func(t *testing.T) {
56		containerSvc := mocks.NewMockContainerService(t)
57		runtime := outMocks.NewMockContainerRuntime(t)
58
59		svc := NewService("", true, containerSvc, runtime, log)
60
61		_, err := svc.GetProcessLogs(context.Background(), 10)
62		assert.Error(t, err)
63		assert.Contains(t, err.Error(), "log file path not configured")
64	})
65
66	t.Run("returns all lines when fewer than requested", func(t *testing.T) {
67		tmpDir := t.TempDir()
68		logPath := filepath.Join(tmpDir, "test.log")
69		content := "line1\nline2\n"
70		err := os.WriteFile(logPath, []byte(content), 0644)
71		require.NoError(t, err)
72
73		containerSvc := mocks.NewMockContainerService(t)
74		runtime := outMocks.NewMockContainerRuntime(t)
75
76		svc := NewService(logPath, true, containerSvc, runtime, log)
77
78		lines, err := svc.GetProcessLogs(context.Background(), 10)
79		require.NoError(t, err)
80		assert.Equal(t, []string{"line1", "line2"}, lines)
81	})
82
83	t.Run("falls back to journalctl when file logging disabled", func(t *testing.T) {
84		origExec := execCommandContext
85		execCommandContext = func(ctx context.Context, name string, args ...string) *exec.Cmd {
86			return exec.CommandContext(ctx, "bash", "-lc", "printf 'journal-line-1\\njournal-line-2\\n'") // #nosec G204
87		}
88		defer func() {
89			execCommandContext = origExec
90		}()
91
92		containerSvc := mocks.NewMockContainerService(t)
93		runtime := outMocks.NewMockContainerRuntime(t)
94
95		svc := NewService("/tmp/unused.log", false, containerSvc, runtime, log)
96
97		lines, err := svc.GetProcessLogs(context.Background(), 10)
98		require.NoError(t, err)
99		assert.Equal(t, []string{"journal-line-1", "journal-line-2"}, lines)
100	})
101}
102
103func TestService_FollowProcessLogs(t *testing.T) {
104	log := zerowrap.New(zerowrap.Config{Level: "warn"})
105
106	t.Run("streams lines and respects context cancellation", func(t *testing.T) {
107		tmpDir := t.TempDir()
108		logPath := filepath.Join(tmpDir, "test.log")
109		content := "initial1\ninitial2\n"
110		err := os.WriteFile(logPath, []byte(content), 0644)
111		require.NoError(t, err)
112
113		containerSvc := mocks.NewMockContainerService(t)
114		runtime := outMocks.NewMockContainerRuntime(t)
115
116		svc := NewService(logPath, true, containerSvc, runtime, log)
117
118		ctx, cancel := context.WithCancel(context.Background())
119		defer cancel()
120
121		ch, err := svc.FollowProcessLogs(ctx, 2)
122		require.NoError(t, err)
123
124		// Read initial lines
125		var received []string
126		for i := 0; i < 2; i++ {
127			select {
128			case line := <-ch:
129				received = append(received, line)
130			case <-time.After(time.Second):
131				t.Fatal("timeout waiting for initial lines")
132			}
133		}
134		assert.Equal(t, []string{"initial1", "initial2"}, received)
135
136		// Cancel context to stop following
137		cancel()
138
139		// Channel should be closed
140		select {
141		case <-ch:
142			// May receive one more line before closure, that's OK
143		case <-time.After(500 * time.Millisecond):
144			// OK, channel may already be closed
145		}
146	})
147}
148
149func TestService_GetContainerLogs(t *testing.T) {
150	log := zerowrap.New(zerowrap.Config{Level: "warn"})
151
152	t.Run("returns error when container not found", func(t *testing.T) {
153		containerSvc := mocks.NewMockContainerService(t)
154		runtime := outMocks.NewMockContainerRuntime(t)
155
156		containerSvc.EXPECT().Get(mock.Anything, "unknown.local").Return(nil, false)
157
158		svc := NewService("/tmp/test.log", true, containerSvc, runtime, log)
159
160		_, err := svc.GetContainerLogs(context.Background(), "unknown.local", 10)
161		assert.Error(t, err)
162		assert.Contains(t, err.Error(), "container not found")
163	})
164
165	t.Run("calls runtime with correct container ID", func(t *testing.T) {
166		containerSvc := mocks.NewMockContainerService(t)
167		runtime := outMocks.NewMockContainerRuntime(t)
168
169		container := &domain.Container{
170			ID:     "abc123",
171			Name:   "app.local",
172			Status: "running",
173		}
174		containerSvc.EXPECT().Get(mock.Anything, "app.local").Return(container, true)
175
176		// Create a mock reader that returns empty content
177		runtime.EXPECT().GetContainerLogs(mock.Anything, "abc123", false).Return(&mockReader{}, nil)
178
179		svc := NewService("/tmp/test.log", true, containerSvc, runtime, log)
180
181		lines, err := svc.GetContainerLogs(context.Background(), "app.local", 10)
182		require.NoError(t, err)
183		assert.Empty(t, lines)
184	})
185}
186
187func TestService_FollowContainerLogs(t *testing.T) {
188	log := zerowrap.New(zerowrap.Config{Level: "warn"})
189
190	t.Run("returns error when container not found", func(t *testing.T) {
191		containerSvc := mocks.NewMockContainerService(t)
192		runtime := outMocks.NewMockContainerRuntime(t)
193
194		containerSvc.EXPECT().Get(mock.Anything, "unknown.local").Return(nil, false)
195
196		svc := NewService("/tmp/test.log", true, containerSvc, runtime, log)
197
198		_, err := svc.FollowContainerLogs(context.Background(), "unknown.local", 10)
199		assert.Error(t, err)
200		assert.Contains(t, err.Error(), "container not found")
201	})
202}
203
204func TestTailLines(t *testing.T) {
205	t.Run("returns correct last N lines", func(t *testing.T) {
206		tmpDir := t.TempDir()
207		path := filepath.Join(tmpDir, "test.log")
208		content := "a\nb\nc\nd\ne\n"
209		err := os.WriteFile(path, []byte(content), 0644)
210		require.NoError(t, err)
211
212		file, err := os.Open(path)
213		require.NoError(t, err)
214		defer file.Close()
215
216		lines, err := tailLines(file, 3)
217		require.NoError(t, err)
218		assert.Equal(t, []string{"c", "d", "e"}, lines)
219	})
220
221	t.Run("returns all lines when n exceeds line count", func(t *testing.T) {
222		tmpDir := t.TempDir()
223		path := filepath.Join(tmpDir, "test.log")
224		content := "x\ny\n"
225		err := os.WriteFile(path, []byte(content), 0644)
226		require.NoError(t, err)
227
228		file, err := os.Open(path)
229		require.NoError(t, err)
230		defer file.Close()
231
232		lines, err := tailLines(file, 10)
233		require.NoError(t, err)
234		assert.Equal(t, []string{"x", "y"}, lines)
235	})
236
237	t.Run("returns empty slice for empty file", func(t *testing.T) {
238		tmpDir := t.TempDir()
239		path := filepath.Join(tmpDir, "test.log")
240		err := os.WriteFile(path, []byte(""), 0644)
241		require.NoError(t, err)
242
243		file, err := os.Open(path)
244		require.NoError(t, err)
245		defer file.Close()
246
247		lines, err := tailLines(file, 10)
248		require.NoError(t, err)
249		assert.Empty(t, lines)
250	})
251
252	t.Run("returns empty slice when n is zero", func(t *testing.T) {
253		tmpDir := t.TempDir()
254		path := filepath.Join(tmpDir, "test.log")
255		content := "a\nb\n"
256		err := os.WriteFile(path, []byte(content), 0644)
257		require.NoError(t, err)
258
259		file, err := os.Open(path)
260		require.NoError(t, err)
261		defer file.Close()
262
263		lines, err := tailLines(file, 0)
264		require.NoError(t, err)
265		assert.Empty(t, lines)
266	})
267}
268
269// mockReader implements io.ReadCloser for testing.
270type mockReader struct{}
271
272func (m *mockReader) Read(p []byte) (n int, err error) {
273	return 0, io.EOF
274}
275
276func (m *mockReader) Close() error {
277	return nil
278}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close