v2.4.2 7da87b2

service_test.go RawHistoryFolder

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

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