1package bootstrap
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 corelogging "github.com/bnema/dumber/internal/logging"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17func TestRedactSensitiveContent(t *testing.T) {
18 t.Run("url_query_params", func(t *testing.T) {
19 line := "redirect to https://example.com/cb?code=abc123&token=secret#frag"
20 redacted := redactSensitiveContent(line)
21 assert.Contains(t, redacted, "https://example.com/cb")
22 assert.NotContains(t, redacted, "code=abc123")
23 assert.NotContains(t, redacted, "token=secret")
24 assert.NotContains(t, redacted, "#frag")
25 })
26
27 t.Run("json_secret_fields", func(t *testing.T) {
28 line := `{"token":"eyJhbGciOi","access_token":"sk-abc123","password":"hunter2"}`
29 redacted := redactSensitiveContent(line)
30 assert.NotContains(t, redacted, "eyJhbGciOi")
31 assert.NotContains(t, redacted, "sk-abc123")
32 assert.NotContains(t, redacted, "hunter2")
33 assert.Contains(t, redacted, `"token":"[REDACTED]"`)
34 assert.Contains(t, redacted, `"access_token":"[REDACTED]"`)
35 assert.Contains(t, redacted, `"password":"[REDACTED]"`)
36 })
37
38 t.Run("key_value_params", func(t *testing.T) {
39 line := "callback received password=secret123 token=abc456"
40 redacted := redactSensitiveContent(line)
41 assert.NotContains(t, redacted, "secret123")
42 assert.NotContains(t, redacted, "abc456")
43 assert.Contains(t, redacted, "password=[REDACTED]")
44 assert.Contains(t, redacted, "token=[REDACTED]")
45 })
46
47 t.Run("authorization_headers", func(t *testing.T) {
48 line := "Authorization: Bearer eyJhbGciOiJSUz"
49 redacted := redactSensitiveContent(line)
50 assert.NotContains(t, redacted, "eyJhbGciOiJSUz")
51 assert.Contains(t, redacted, "[REDACTED]")
52
53 line2 := "auth: basic dXNlcjpwYXNz"
54 redacted2 := redactSensitiveContent(line2)
55 assert.NotContains(t, redacted2, "dXNlcjpwYXNz")
56 assert.Contains(t, redacted2, "[REDACTED]")
57 })
58}
59
60func TestReadRedactedLogTail_RingBuffer(t *testing.T) {
61 dir := t.TempDir()
62 path := filepath.Join(dir, "test.log")
63
64 var lines []string
65 for i := 0; i < 10; i++ {
66 lines = append(lines, fmt.Sprintf("line %d", i))
67 }
68 require.NoError(t, os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), markerFilePerm))
69
70 t.Run("fewer_lines_than_requested", func(t *testing.T) {
71 result := readRedactedLogTail(path, 20)
72 assert.Len(t, result, 10)
73 assert.Equal(t, "line 0", result[0])
74 assert.Equal(t, "line 9", result[9])
75 })
76
77 t.Run("exact_tail", func(t *testing.T) {
78 result := readRedactedLogTail(path, 3)
79 assert.Len(t, result, 3)
80 assert.Equal(t, "line 7", result[0])
81 assert.Equal(t, "line 8", result[1])
82 assert.Equal(t, "line 9", result[2])
83 })
84}
85
86func TestWriteUnexpectedCloseReport(t *testing.T) {
87 lockDir := t.TempDir()
88 sessionID := "crash-test-1"
89 startedAt := time.Date(2026, 2, 8, 10, 0, 0, 0, time.UTC)
90
91 require.NoError(t, writeStartupMarker(lockDir, sessionID, startedAt))
92 _, err := markAbruptExits(lockDir, startedAt.Add(3*time.Minute), nil)
93 require.NoError(t, err)
94
95 logPath := filepath.Join(lockDir, corelogging.SessionFilename(sessionID))
96 logBody := `{"level":"info","message":"opening https://example.com/path?a=1&b=2"}` +
97 "\n" +
98 `{"level":"warn","message":"callback code=abc token=def"}` +
99 "\n"
100 require.NoError(t, os.WriteFile(logPath, []byte(logBody), markerFilePerm))
101
102 jsonPath, err := writeUnexpectedCloseReport(lockDir, sessionID)
103 require.NoError(t, err)
104 require.NotEmpty(t, jsonPath)
105 assert.FileExists(t, jsonPath)
106
107 mdPath := strings.TrimSuffix(jsonPath, ".json") + ".md"
108 assert.FileExists(t, mdPath)
109
110 raw, err := os.ReadFile(jsonPath)
111 require.NoError(t, err)
112
113 var payload map[string]any
114 require.NoError(t, json.Unmarshal(raw, &payload))
115 assert.Equal(t, sessionID, payload["session_id"])
116
117 classificationRaw, ok := payload["classification"].(map[string]any)
118 require.True(t, ok, "classification should be a map[string]any")
119 classValue, ok := classificationRaw["class"].(string)
120 require.True(t, ok, "classification.class should be a string")
121 assert.Equal(t, string(SessionExitMainProcessCrashOrAbrupt), classValue)
122
123 tail, ok := payload["session_log_tail_redacted"].([]any)
124 require.True(t, ok, "session_log_tail_redacted should be a []any")
125 require.GreaterOrEqual(t, len(tail), 2, "expected at least 2 log tail entries")
126 first, ok := tail[0].(string)
127 require.True(t, ok, "first tail entry should be a string")
128 last, ok := tail[len(tail)-1].(string)
129 require.True(t, ok, "last tail entry should be a string")
130 text := first + last
131 assert.NotContains(t, text, "?a=1&b=2")
132 assert.NotContains(t, text, "code=abc")
133 assert.NotContains(t, text, "token=def")
134}