190 lines
5.7 KiB
Go
190 lines
5.7 KiB
Go
package log
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Tests the integration between the log package and the formatter/sanitizer packages.
|
|
|
|
// TestFormatterIntegration verifies each format reaches the file writer intact.
|
|
func TestFormatterIntegration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
format string
|
|
checks []string
|
|
}{
|
|
{"txt", "txt", []string{`INFO "test message"`}},
|
|
{"json", "json", []string{`"level":"INFO"`, `"fields":["test message"]`}},
|
|
{"raw", "raw", []string{"test message"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
logger := NewLogger()
|
|
|
|
cfg := DefaultConfig()
|
|
cfg.Directory = tmpDir
|
|
cfg.Format = tt.format
|
|
cfg.ShowTimestamp = false
|
|
cfg.ShowLevel = true
|
|
cfg.EnableConsole = false
|
|
cfg.EnableFile = true
|
|
cfg.FlushIntervalMs = 10
|
|
|
|
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
|
mustNoErr(t, logger.Start(), "Start")
|
|
t.Cleanup(func() { _ = logger.Shutdown() })
|
|
|
|
logger.Info("test message")
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
|
|
mustEventually(t, time.Second, "record written", func() bool {
|
|
return len(readLog(t, tmpDir)) > 0
|
|
})
|
|
|
|
content := readLog(t, tmpDir)
|
|
for _, want := range tt.checks {
|
|
contains(t, content, want, tt.format+" output")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStructuredJSONOutput verifies FlagStructuredJSON emits a message key and a
|
|
// marshaled field object rather than a positional fields array.
|
|
func TestStructuredJSONOutput(t *testing.T) {
|
|
logger, tmpDir := newTestLogger(t)
|
|
|
|
cfg := logger.GetConfig()
|
|
cfg.Format = "json"
|
|
cfg.ShowTimestamp = false
|
|
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
|
|
|
logger.LogStructured(LevelInfo, "structured log", map[string]any{
|
|
"user_id": 123,
|
|
"action": "login",
|
|
"success": true,
|
|
})
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
|
|
mustEventually(t, time.Second, "record written", func() bool {
|
|
return strings.Contains(readLog(t, tmpDir), "structured log")
|
|
})
|
|
|
|
content := readLog(t, tmpDir)
|
|
contains(t, content, `"message":"structured log"`, "message key")
|
|
// json.Marshal orders map keys lexically
|
|
contains(t, content, `"fields":{"action":"login","success":true,"user_id":123}`, "field object")
|
|
notContains(t, content, `"fields":[`, "structured branch must not fall through to the array form")
|
|
}
|
|
|
|
// TestControlCharacterSanitization verifies PolicyTxt hex-encodes every
|
|
// non-printable rune on the raw output path. Tab and DEL are non-printable per
|
|
// strconv.IsPrint and are encoded like any other control byte.
|
|
func TestControlCharacterSanitization(t *testing.T) {
|
|
logger, tmpDir := newTestLogger(t)
|
|
|
|
cfg := logger.GetConfig()
|
|
cfg.Format = "raw"
|
|
cfg.ShowTimestamp = false
|
|
cfg.ShowLevel = false
|
|
cfg.Sanitization = PolicyTxt
|
|
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
}{
|
|
{"null bytes", "test\x00data", "test<00>data"},
|
|
{"bell", "alert\x07message", "alert<07>message"},
|
|
{"backspace", "back\x08space", "back<08>space"},
|
|
{"form feed", "page\x0Cbreak", "page<0c>break"},
|
|
{"vertical tab", "vertical\x0Btab", "vertical<0b>tab"},
|
|
{"tab", "col1\tcol2", "col1<09>col2"},
|
|
{"escape", "escape\x1B[31mcolor", "escape<1b>[31mcolor"},
|
|
{"del", "del\x7Fmark", "del<7f>mark"},
|
|
{"mixed", "\x00\x01\x02test\x1F\x7Fdata", "<00><01><02>test<1f><7f>data"},
|
|
// '<' is encoded so input cannot forge a hex marker
|
|
{"literal angle bracket", "a<00>b", "a<3c>00>b"},
|
|
{"utf8 untouched", "Hello │ 世界", "Hello │ 世界"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
logger.Message(tc.input)
|
|
}
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
|
|
// Records append in submission order; the last one gates the read
|
|
last := testCases[len(testCases)-1].expected
|
|
mustEventually(t, time.Second, "all records written", func() bool {
|
|
return strings.Contains(readLog(t, tmpDir), last)
|
|
})
|
|
|
|
content := readLog(t, tmpDir)
|
|
for _, tc := range testCases {
|
|
contains(t, content, tc.expected, tc.name)
|
|
}
|
|
}
|
|
|
|
// TestRawSanitizedOutput verifies raw format emits space-joined arguments with
|
|
// no framing, and that sanitization applies per argument across string and []byte.
|
|
func TestRawSanitizedOutput(t *testing.T) {
|
|
logger, tmpDir := newTestLogger(t)
|
|
|
|
cfg := logger.GetConfig()
|
|
cfg.Format = "raw"
|
|
cfg.ShowTimestamp = false
|
|
cfg.ShowLevel = false
|
|
cfg.Sanitization = PolicyTxt
|
|
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
|
|
|
const (
|
|
utf8String = "Hello │ 世界"
|
|
stringWithCtl = "start-\x07-end"
|
|
multiByteControl = "line1\u0085line2"
|
|
)
|
|
bytesWithCtl := []byte("data\x00with\x08bytes")
|
|
|
|
// U+0085 is a single non-printable rune; its two UTF-8 bytes encode as one marker
|
|
want := strings.Join([]string{
|
|
utf8String,
|
|
"start-<07>-end",
|
|
"data<00>with<08>bytes",
|
|
"line1<c285>line2",
|
|
}, " ")
|
|
|
|
logger.Message(utf8String, stringWithCtl, bytesWithCtl, multiByteControl)
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
|
|
mustEventually(t, time.Second, "record written", func() bool {
|
|
return len(readLog(t, tmpDir)) > 0
|
|
})
|
|
|
|
equal(t, readLog(t, tmpDir), want, "raw output must match exactly")
|
|
}
|
|
|
|
// TestPolicyRawPassthrough verifies the default policy performs no substitution.
|
|
func TestPolicyRawPassthrough(t *testing.T) {
|
|
logger, tmpDir := newTestLogger(t)
|
|
|
|
cfg := logger.GetConfig()
|
|
cfg.Format = "raw"
|
|
cfg.ShowTimestamp = false
|
|
cfg.ShowLevel = false
|
|
cfg.Sanitization = PolicyRaw
|
|
mustNoErr(t, logger.ApplyConfig(cfg), "ApplyConfig")
|
|
|
|
logger.Message("esc\x1b[31mred")
|
|
mustNoErr(t, logger.Flush(time.Second), "Flush")
|
|
|
|
mustEventually(t, time.Second, "record written", func() bool {
|
|
return len(readLog(t, tmpDir)) > 0
|
|
})
|
|
|
|
equal(t, readLog(t, tmpDir), "esc\x1b[31mred", "PolicyRaw must not transform input")
|
|
}
|