v0.30.0 eb3068f

logger_test.go RawHistoryFolder

1package logging
2
3import (
4	"os"
5	"path/filepath"
6	"strings"
7	"testing"
8
9	"github.com/rs/zerolog"
10)
11
12func TestRedactURLUsesHostOnlyForAbsoluteURLs(t *testing.T) {
13	t.Parallel()
14
15	got := RedactURL("https://user:pass@example.com/private/path?token=secret#frag")
16	if got != "example.com" {
17		t.Fatalf("RedactURL returned %q, want %q", got, "example.com")
18	}
19}
20
21func TestRedactURLRedactsLocalOrOpaqueInputs(t *testing.T) {
22	t.Parallel()
23
24	for _, raw := range []string{"/home/user/private/file.html?token=secret", "not a url"} {
25		if got := RedactURL(raw); got != RedactedURLPlaceholder {
26			t.Fatalf("RedactURL(%q) returned %q, want %q", raw, got, RedactedURLPlaceholder)
27		}
28	}
29}
30
31func TestTruncateURLRedactsBeforeTruncating(t *testing.T) {
32	t.Parallel()
33
34	got := TruncateURL("https://example.com/private/path?token=secret#frag", 96)
35	if got != "example.com" {
36		t.Fatalf("TruncateURL returned %q, want %q", got, "example.com")
37	}
38	if strings.Contains(got, "token") || strings.Contains(got, "/private") || strings.Contains(got, "frag") {
39		t.Fatalf("TruncateURL leaked sensitive URL data: %q", got)
40	}
41}
42
43func TestSafeURLHostDropsPathQueryAndFragment(t *testing.T) {
44	t.Parallel()
45
46	got := SafeURLHost("https://example.com/private/path?token=secret#frag")
47	if got != "example.com" {
48		t.Fatalf("SafeURLHost returned %q, want %q", got, "example.com")
49	}
50}
51
52func TestSafeURLHostReturnsEmptyForOpaqueOrInvalidInput(t *testing.T) {
53	t.Parallel()
54
55	for _, raw := range []string{"about:blank", "not a url"} {
56		if got := SafeURLHost(raw); got != "" {
57			t.Fatalf("SafeURLHost(%q) returned %q, want empty host", raw, got)
58		}
59	}
60}
61
62func TestRedactURLStripsQueryFragmentFromAboutOpaqueURLs(t *testing.T) {
63	t.Parallel()
64
65	got := RedactURL("about:blank?token=secret#frag")
66	if want := "about:blank"; got != want {
67		t.Fatalf("RedactURL(about:blank?token=secret#frag) = %q, want %q", got, want)
68	}
69	if strings.Contains(got, "token") || strings.Contains(got, "secret") || strings.Contains(got, "frag") {
70		t.Fatalf("RedactURL leaked query/fragment from about: opaque URL: %q", got)
71	}
72}
73
74func TestRedactURLRedactsFileURLPathQueryFragment(t *testing.T) {
75	t.Parallel()
76
77	got := RedactURL("file:///home/user/private?token=secret#frag")
78	if got != "file:"+RedactedURLPlaceholder {
79		t.Fatalf("RedactURL(file:///home/user/private?token=secret#frag) = %q, want %q", got, "file:"+RedactedURLPlaceholder)
80	}
81	if strings.Contains(got, "home") || strings.Contains(got, "user") || strings.Contains(got, "private") {
82		t.Fatalf("RedactURL leaked file path from file:// URL: %q", got)
83	}
84	if strings.Contains(got, "token") || strings.Contains(got, "secret") || strings.Contains(got, "frag") {
85		t.Fatalf("RedactURL leaked query/fragment from file:// URL: %q", got)
86	}
87}
88
89func TestRedactURLReturnsPlaceholderForInvalidParse(t *testing.T) {
90	t.Parallel()
91
92	got := RedactURL("http://[::1")
93	if got != RedactedURLPlaceholder {
94		t.Fatalf("RedactURL(http://[::1) = %q, want %q", got, RedactedURLPlaceholder)
95	}
96}
97
98func TestNewWithFileCreatesUserPrivateLogDirectoryAndFile(t *testing.T) {
99	t.Parallel()
100
101	parent := t.TempDir()
102	logDir := filepath.Join(parent, "logs")
103	logger, cleanup, err := NewWithFile(Config{Level: zerolog.InfoLevel, Format: logFormatJSON}, FileConfig{
104		Enabled:   true,
105		LogDir:    logDir,
106		SessionID: "test-session",
107	})
108	if err != nil {
109		t.Fatalf("NewWithFile: %v", err)
110	}
111	defer cleanup()
112
113	logger.Info().Msg("hello")
114	cleanup()
115
116	dirInfo, err := os.Stat(logDir)
117	if err != nil {
118		t.Fatalf("stat log dir: %v", err)
119	}
120	if got := dirInfo.Mode().Perm(); got != 0o700 {
121		t.Fatalf("log dir permissions = %o, want 700", got)
122	}
123
124	fileInfo, err := os.Stat(filepath.Join(logDir, SessionFilename("test-session")))
125	if err != nil {
126		t.Fatalf("stat log file: %v", err)
127	}
128	if got := fileInfo.Mode().Perm(); got != 0o600 {
129		t.Fatalf("log file permissions = %o, want 600", got)
130	}
131}
132
133func TestSessionFileWriter_DropsWritesAfterClose(t *testing.T) {
134	t.Parallel()
135
136	dir := t.TempDir()
137	path := filepath.Join(dir, "session.log")
138
139	file, err := os.Create(path)
140	if err != nil {
141		t.Fatalf("create temp log file: %v", err)
142	}
143
144	writer := newSessionFileWriter(file)
145
146	n, err := writer.Write([]byte("first\n"))
147	if err != nil {
148		t.Fatalf("write before close: %v", err)
149	}
150	if n != len("first\n") {
151		t.Fatalf("write before close returned %d, want %d", n, len("first\n"))
152	}
153
154	if closeErr := writer.Close(); closeErr != nil {
155		t.Fatalf("close writer: %v", closeErr)
156	}
157
158	n, err = writer.Write([]byte("second\n"))
159	if err != nil {
160		t.Fatalf("write after close: %v", err)
161	}
162	if n != len("second\n") {
163		t.Fatalf("write after close returned %d, want %d", n, len("second\n"))
164	}
165
166	data, err := os.ReadFile(path)
167	if err != nil {
168		t.Fatalf("read log file: %v", err)
169	}
170	if got, want := string(data), "first\n"; got != want {
171		t.Fatalf("log file contents = %q, want %q", got, want)
172	}
173}
174
175func TestSessionFileWriter_IgnoresExternallyClosedFile(t *testing.T) {
176	t.Parallel()
177
178	dir := t.TempDir()
179	path := filepath.Join(dir, "session.log")
180
181	file, err := os.Create(path)
182	if err != nil {
183		t.Fatalf("create temp log file: %v", err)
184	}
185
186	writer := newSessionFileWriter(file)
187	if closeErr := file.Close(); closeErr != nil {
188		t.Fatalf("close underlying file: %v", closeErr)
189	}
190
191	n, err := writer.Write([]byte("late\n"))
192	if err != nil {
193		t.Fatalf("write after external close: %v", err)
194	}
195	if n != len("late\n") {
196		t.Fatalf("write after external close returned %d, want %d", n, len("late\n"))
197	}
198}

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 8

Tags 42

↑↓ moveenter openesc close