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}