252 lines
7.9 KiB
Go
252 lines
7.9 KiB
Go
package formatter
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/lixenwraith/log/sanitizer"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestFormatter(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
t.Run("fluent API", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).
|
|
Type("json").
|
|
TimestampFormat(time.RFC3339).
|
|
ShowLevel(true).
|
|
ShowTimestamp(true)
|
|
|
|
data := f.Format(0, timestamp, 0, "", []any{"test"})
|
|
assert.Contains(t, string(data), `"level":"INFO"`)
|
|
assert.Contains(t, string(data), `"time":"2024-01-01T12:00:00Z"`)
|
|
})
|
|
|
|
t.Run("txt format", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("txt")
|
|
|
|
data := f.Format(FlagDefault, timestamp, 0, "", []any{"test message", 123})
|
|
str := string(data)
|
|
|
|
assert.Contains(t, str, "2024-01-01")
|
|
assert.Contains(t, str, "INFO")
|
|
assert.Contains(t, str, "test message")
|
|
assert.Contains(t, str, "123")
|
|
assert.True(t, strings.HasSuffix(str, "\n"))
|
|
})
|
|
|
|
t.Run("json format", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("json")
|
|
|
|
data := f.Format(FlagDefault, timestamp, 4, "trace1", []any{"warning", true})
|
|
|
|
var result map[string]any
|
|
err := json.Unmarshal(data[:len(data)-1], &result) // Remove trailing newline
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "WARN", result["level"])
|
|
assert.Equal(t, "trace1", result["trace"])
|
|
fields := result["fields"].([]any)
|
|
assert.Equal(t, "warning", fields[0])
|
|
assert.Equal(t, true, fields[1])
|
|
})
|
|
|
|
t.Run("raw format", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("raw")
|
|
|
|
data := f.FormatWithOptions("raw", 0, timestamp, 0, "", []any{"raw", "data", 42})
|
|
str := string(data)
|
|
|
|
assert.Equal(t, "raw data 42", str)
|
|
assert.False(t, strings.HasSuffix(str, "\n"))
|
|
})
|
|
|
|
t.Run("flag override raw", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("json") // Configure as JSON
|
|
|
|
data := f.Format(FlagRaw, timestamp, 0, "", []any{"forced", "raw"})
|
|
str := string(data)
|
|
|
|
assert.Equal(t, "forced raw", str)
|
|
})
|
|
|
|
t.Run("structured json", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyJSON)
|
|
f := New(s).Type("json")
|
|
|
|
fields := map[string]any{"key1": "value1", "key2": 42}
|
|
data := f.Format(FlagStructuredJSON|FlagDefault, timestamp, 0, "",
|
|
[]any{"structured message", fields})
|
|
|
|
var result map[string]any
|
|
err := json.Unmarshal(data[:len(data)-1], &result)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "structured message", result["message"])
|
|
assert.Equal(t, map[string]any{"key1": "value1", "key2": float64(42)}, result["fields"])
|
|
})
|
|
|
|
t.Run("special characters escaping", func(t *testing.T) {
|
|
// PolicyRaw — transport escaping applies exactly once.
|
|
// PolicyJSON + json format double-escapes (see TestJSONSanitizerLayering).
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("json")
|
|
|
|
data := f.Format(FlagDefault, timestamp, 0, "",
|
|
[]any{"test\n\r\t\"\\message"})
|
|
|
|
str := string(data)
|
|
assert.Contains(t, str, `test\n\r\t\"\\message`)
|
|
})
|
|
|
|
t.Run("error type handling", func(t *testing.T) {
|
|
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
|
|
f := New(s).Type("txt")
|
|
|
|
err := errors.New("test error")
|
|
data := f.Format(FlagDefault, timestamp, 8, "", []any{err})
|
|
|
|
str := string(data)
|
|
assert.Contains(t, str, "test error")
|
|
})
|
|
}
|
|
|
|
func TestJSONUTF8Passthrough(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
f := New(sanitizer.New()).Type("json")
|
|
in := "héllo 世界 ✓"
|
|
data := f.Format(FlagDefault, timestamp, 0, "", []any{in})
|
|
|
|
var result map[string]any
|
|
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &result))
|
|
assert.Equal(t, in, result["fields"].([]any)[0])
|
|
assert.NotContains(t, string(data), `\u00`, "no per-byte escapes of UTF-8")
|
|
}
|
|
|
|
func TestJSONSanitizerLayering(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
|
|
// Content transform (PolicyTxt) applied before transport escaping
|
|
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type("json")
|
|
data := f.Format(FlagDefault, timestamp, 0, "", []any{"a\x07b"})
|
|
var result map[string]any
|
|
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &result))
|
|
assert.Equal(t, "a<07>b", result["fields"].([]any)[0])
|
|
|
|
// PolicyJSON + json format: content transform emits literal backslash
|
|
// sequences; transport escaping preserves them (double-escape by design)
|
|
f2 := New(sanitizer.New().Policy(sanitizer.PolicyJSON)).Type("json")
|
|
data2 := f2.Format(FlagDefault, timestamp, 0, "", []any{"a\nb"})
|
|
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data2, []byte("\n")), &result))
|
|
assert.Equal(t, `a\nb`, result["fields"].([]any)[0])
|
|
}
|
|
|
|
func TestFlagResolution(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
f := New(sanitizer.New()).Type("txt").ShowTimestamp(true).ShowLevel(true)
|
|
|
|
// Non-display flags alone inherit configured defaults
|
|
str := string(f.Format(FlagStructuredJSON, timestamp, 0, "", []any{"m"}))
|
|
assert.Contains(t, str, "2024-01-01")
|
|
assert.Contains(t, str, "INFO")
|
|
|
|
// Explicit suppression
|
|
str = string(f.Format(FlagNoLevel, timestamp, 0, "", []any{"m"}))
|
|
assert.Contains(t, str, "2024-01-01")
|
|
assert.NotContains(t, str, "INFO")
|
|
|
|
str = string(f.Format(FlagNoTimestamp|FlagNoLevel, timestamp, 0, "", []any{"m"}))
|
|
assert.NotContains(t, str, "2024-01-01")
|
|
assert.NotContains(t, str, "INFO")
|
|
|
|
// FormatWithOptions is fully explicit: unset Show bits mean off
|
|
str = string(f.FormatWithOptions("txt", 0, timestamp, 0, "", []any{"m"}))
|
|
assert.NotContains(t, str, "INFO")
|
|
}
|
|
|
|
func TestUnknownFormatFallback(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
f := New(sanitizer.New()).Type("txt")
|
|
data := f.FormatWithOptions("xml", FlagShowLevel, timestamp, 8, "", []any{"boom"})
|
|
require.NotNil(t, data)
|
|
assert.Contains(t, string(data), "ERROR")
|
|
assert.Contains(t, string(data), "boom")
|
|
}
|
|
|
|
func TestReturnedSliceInvalidation(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
f := New(sanitizer.New()).Type("txt").ShowTimestamp(false).ShowLevel(false)
|
|
|
|
first := f.Format(0, timestamp, 0, "", []any{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
|
|
snapshot := string(first)
|
|
_ = f.Format(0, timestamp, 0, "", []any{"b"})
|
|
assert.NotEqual(t, snapshot, string(first),
|
|
"buffered Format output is invalidated by the next buffered call")
|
|
}
|
|
|
|
func TestAppendFormatStable(t *testing.T) {
|
|
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
|
f := New(sanitizer.New()).Type("txt").ShowTimestamp(false).ShowLevel(false)
|
|
|
|
first := f.AppendFormat(nil, 0, timestamp, 0, "", []any{"first-payload"})
|
|
snapshot := string(first)
|
|
_ = f.Format(0, timestamp, 0, "", []any{"interleaved-buffered-call"})
|
|
second := f.AppendFormat(nil, 0, timestamp, 0, "", []any{"second"})
|
|
|
|
assert.Equal(t, snapshot, string(first), "caller-owned buffer unaffected by buffered calls")
|
|
assert.Equal(t, "second\n", string(second))
|
|
}
|
|
|
|
func TestFormatterConcurrentAppend(t *testing.T) {
|
|
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type("json")
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 16; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
for j := 0; j < 200; j++ {
|
|
out := f.AppendFormat(nil, FlagDefault, time.Now(), 0, "", []any{"w", id, "i", j, "s", "x\x00y"})
|
|
if !json.Valid(bytes.TrimSuffix(out, []byte("\n"))) {
|
|
t.Errorf("invalid JSON: %s", out)
|
|
return
|
|
}
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestLevelToString(t *testing.T) {
|
|
tests := []struct {
|
|
level int64
|
|
expected string
|
|
}{
|
|
{-4, "DEBUG"},
|
|
{0, "INFO"},
|
|
{4, "WARN"},
|
|
{8, "ERROR"},
|
|
{12, "PROC"},
|
|
{16, "DISK"},
|
|
{20, "SYS"},
|
|
{999, "LEVEL(999)"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.expected, func(t *testing.T) {
|
|
assert.Equal(t, tt.expected, LevelToString(tt.level))
|
|
})
|
|
}
|
|
}
|