]> git.giacomolli.uk - onechat/blob - daemon_log_test.go
Write private data archives with safe extraction
[onechat] / daemon_log_test.go
1 package main
2
3 import (
4         "context"
5         "errors"
6         "io"
7         "log/slog"
8         "strings"
9         "sync"
10         "testing"
11         "time"
12
13         "github.com/stretchr/testify/require"
14 )
15
16 func TestDaemonLogFormatsLevelsAndPrefixes(t *testing.T) {
17         testCases := []struct {
18                 name     string
19                 level    slog.Level
20                 prefix   string
21                 priority string
22         }{
23                 {
24                         name:     "error names the failed part",
25                         level:    slog.LevelError,
26                         prefix:   "onechat: ",
27                         priority: "3",
28                 },
29                 {
30                         name:     "warning names the retried part",
31                         level:    slog.LevelWarn,
32                         prefix:   "telegram: ",
33                         priority: "4",
34                 },
35                 {
36                         name:     "info names the connected part",
37                         level:    slog.LevelInfo,
38                         prefix:   "whatsapp: ",
39                         priority: "6",
40                 },
41                 {
42                         name:     "debug has its own priority",
43                         level:    slog.LevelDebug,
44                         prefix:   "onechat: ",
45                         priority: "7",
46                 },
47         }
48
49         for _, tc := range testCases {
50                 t.Run(tc.name, func(t *testing.T) {
51                         for _, journal := range []bool{false, true} {
52                                 var out strings.Builder
53
54                                 handler := &DaemonLogHandler{
55                                         out:     &out,
56                                         prefix:  tc.prefix,
57                                         journal: journal,
58                                         mutex:   &sync.Mutex{},
59                                 }
60
61                                 record := slog.NewRecord(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), tc.level, "ready", 0)
62                                 require.True(t, handler.Enabled(t.Context(), tc.level))
63                                 require.NoError(t, handler.Handle(t.Context(), record))
64
65                                 expected := "2026/01/02 03:04:05 " + tc.level.String() + " " + tc.prefix + "ready\n"
66                                 if journal {
67                                         expected = "<" + tc.priority + ">" + tc.prefix + "ready\n"
68                                 }
69
70                                 require.Equal(t, expected, out.String())
71                         }
72                 })
73         }
74 }
75
76 func TestDaemonLogKeepsAttributesInOrderAndCauseLast(t *testing.T) {
77         var out strings.Builder
78
79         handler := &DaemonLogHandler{
80                 out:     &out,
81                 prefix:  "telegram: ",
82                 journal: true,
83                 mutex:   &sync.Mutex{},
84         }
85
86         logger := slog.New(handler).With("first", "two words", slog.Attr{})
87         logger.Warn("receive failed", "err", errors.New("502 Bad Gateway"), "retry", 2040*time.Millisecond, "count", 3, "short", 1500*time.Microsecond)
88         require.Equal(t, "<4>telegram: receive failed first=two words retry=2s count=3 short=2ms: 502 Bad Gateway\n", out.String())
89 }
90
91 func TestDaemonLogOmitsEmptyAttributesAndNilCause(t *testing.T) {
92         var out strings.Builder
93
94         handler := &DaemonLogHandler{
95                 out:     &out,
96                 prefix:  "onechat: ",
97                 journal: true,
98                 mutex:   &sync.Mutex{},
99         }
100
101         slog.New(handler).Info("ready", slog.Attr{}, slog.Group("empty"), "err", nil)
102         require.Equal(t, "<6>onechat: ready\n", out.String())
103 }
104
105 func TestDaemonLogGroupsDoNotChangeOtherLoggers(t *testing.T) {
106         var out strings.Builder
107
108         handler := &DaemonLogHandler{
109                 out:     &out,
110                 journal: true,
111                 mutex:   &sync.Mutex{},
112         }
113
114         logger := slog.New(handler)
115         grouped := logger.With("before", 1).WithGroup("outer").With("saved", 2).WithGroup("inner")
116         grouped.Info("done", slog.Group("", "count", 3))
117         logger.Info("ready")
118         require.Equal(t, "<6>done before=1 outer.saved=2 outer.inner.count=3\n<6>ready\n", out.String())
119 }
120
121 func TestDaemonLogReturnsWriteFailure(t *testing.T) {
122         handler := &DaemonLogHandler{
123                 out:   DaemonLogBrokenWriter{},
124                 mutex: &sync.Mutex{},
125         }
126
127         err := handler.Handle(context.Background(), slog.NewRecord(time.Now(), slog.LevelInfo, "ready", 0))
128         require.ErrorIs(t, err, io.ErrClosedPipe)
129         require.ErrorContains(t, err, "write daemon log")
130 }
131
132 type DaemonLogBrokenWriter struct{}
133
134 func (w DaemonLogBrokenWriter) Write(_ []byte) (int, error) {
135         return 0, io.ErrClosedPipe
136 }