1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
|
package config
import (
"os"
"path/filepath"
"testing"
)
func TestParseConfigLoadsDefaultXDGConfig(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
configPath := filepath.Join(home, ".config", "dtail", "dtail.conf")
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
t.Fatalf("mkdir failed: %v", err)
}
writeTestConfig(t, configPath, `{"Common":{"LogLevel":"debug"}}`)
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
if err := in.parseConfig(&Args{}); err != nil {
t.Fatalf("parseConfig failed: %v", err)
}
if in.Common.LogLevel != "debug" {
t.Fatalf("expected log level debug, got %q", in.Common.LogLevel)
}
}
// TestParseConfigFirstWins verifies that when both candidate config files
// exist, the XDG path (~/.config/dtail/dtail.conf) takes precedence and the
// second file (~/.dtail.conf) is ignored entirely — no silent merging.
func TestParseConfigFirstWins(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
xdgPath := filepath.Join(home, ".config", "dtail", "dtail.conf")
if err := os.MkdirAll(filepath.Dir(xdgPath), 0o755); err != nil {
t.Fatalf("mkdir failed: %v", err)
}
writeTestConfig(t, xdgPath, `{"Common":{"LogLevel":"warn"}}`)
homePath := filepath.Join(home, ".dtail.conf")
// The second file would override LogLevel to "error" if merging occurred.
writeTestConfig(t, homePath, `{"Common":{"LogLevel":"error"}}`)
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
if err := in.parseConfig(&Args{}); err != nil {
t.Fatalf("parseConfig failed: %v", err)
}
// First-wins: the XDG config must have set the level; the home config
// must have been skipped, so "error" must NOT appear.
if in.Common.LogLevel != "warn" {
t.Fatalf("expected log level warn (first file wins), got %q", in.Common.LogLevel)
}
}
// TestParseConfigFallsBackToHomeConfig verifies that when only the legacy
// ~/.dtail.conf exists it is loaded as the effective configuration.
func TestParseConfigFallsBackToHomeConfig(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
// Only create the fallback file; the XDG directory does not exist.
homePath := filepath.Join(home, ".dtail.conf")
writeTestConfig(t, homePath, `{"Common":{"LogLevel":"error"}}`)
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
if err := in.parseConfig(&Args{}); err != nil {
t.Fatalf("parseConfig failed: %v", err)
}
if in.Common.LogLevel != "error" {
t.Fatalf("expected log level error from fallback config, got %q", in.Common.LogLevel)
}
}
// TestParseConfigNoConfigFile verifies that parseConfig succeeds without
// error when neither candidate config file is present.
func TestParseConfigNoConfigFile(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
// No config files created; must return nil, not an error.
if err := in.parseConfig(&Args{}); err != nil {
t.Fatalf("expected no error when no config file exists, got: %v", err)
}
}
// TestResolveSSHKeyPath verifies the three-level precedence used when
// resolving the effective SSH private key path.
func TestResolveSSHKeyPath(t *testing.T) {
tests := []struct {
name string
cli string
authKey string
legacy string
expected string
}{
{
name: "cli flag wins over both env vars",
cli: "/cli/key",
authKey: "/auth/key",
legacy: "/legacy/key",
expected: "/cli/key",
},
{
name: "DTAIL_AUTH_KEY_PATH wins over legacy when cli is empty",
cli: "",
authKey: "/auth/key",
legacy: "/legacy/key",
expected: "/auth/key",
},
{
name: "DTAIL_SSH_PRIVATE_KEYFILE_PATH used when auth key env is also empty",
cli: "",
authKey: "",
legacy: "/legacy/key",
expected: "/legacy/key",
},
{
name: "all empty returns empty string",
cli: "",
authKey: "",
legacy: "",
expected: "",
},
{
name: "cli wins when only cli is set",
cli: "/cli/key",
authKey: "",
legacy: "",
expected: "/cli/key",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := resolveSSHKeyPath(tc.cli, tc.authKey, tc.legacy)
if got != tc.expected {
t.Fatalf("resolveSSHKeyPath(%q, %q, %q) = %q; want %q",
tc.cli, tc.authKey, tc.legacy, got, tc.expected)
}
})
}
}
// TestProcessEnvVarsAuthKeyPathTakesPrecedence is a negative/regression test
// that confirms the bug described in task k6 is fixed: when both
// DTAIL_AUTH_KEY_PATH and DTAIL_SSH_PRIVATE_KEYFILE_PATH are set,
// DTAIL_AUTH_KEY_PATH must win.
func TestProcessEnvVarsAuthKeyPathTakesPrecedence(t *testing.T) {
t.Setenv("DTAIL_AUTH_KEY_PATH", "/env/auth/key")
t.Setenv("DTAIL_SSH_PRIVATE_KEYFILE_PATH", "/env/legacy/key")
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
args := &Args{}
in.processEnvVars(args)
if args.SSHPrivateKeyFilePath != "/env/auth/key" {
t.Fatalf("expected DTAIL_AUTH_KEY_PATH to win, got %q", args.SSHPrivateKeyFilePath)
}
}
// TestProcessEnvVarsLegacyFallback verifies that DTAIL_SSH_PRIVATE_KEYFILE_PATH
// is still applied when DTAIL_AUTH_KEY_PATH is not set.
func TestProcessEnvVarsLegacyFallback(t *testing.T) {
t.Setenv("DTAIL_AUTH_KEY_PATH", "")
t.Setenv("DTAIL_SSH_PRIVATE_KEYFILE_PATH", "/env/legacy/key")
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
args := &Args{}
in.processEnvVars(args)
if args.SSHPrivateKeyFilePath != "/env/legacy/key" {
t.Fatalf("expected legacy env var to be used, got %q", args.SSHPrivateKeyFilePath)
}
}
// TestProcessEnvVarsCLIFlagNotOverridden verifies that an explicit CLI flag
// value is not overridden by either environment variable.
func TestProcessEnvVarsCLIFlagNotOverridden(t *testing.T) {
t.Setenv("DTAIL_AUTH_KEY_PATH", "/env/auth/key")
t.Setenv("DTAIL_SSH_PRIVATE_KEYFILE_PATH", "/env/legacy/key")
in := initializer{
Common: newDefaultCommonConfig(),
Server: newDefaultServerConfig(),
Client: newDefaultClientConfig(),
}
args := &Args{SSHPrivateKeyFilePath: "/cli/explicit/key"}
in.processEnvVars(args)
if args.SSHPrivateKeyFilePath != "/cli/explicit/key" {
t.Fatalf("expected CLI flag to be preserved, got %q", args.SSHPrivateKeyFilePath)
}
}
// TestDefaultAuthKeyPathNoLiteralTilde is a regression test for the bug
// described in task l6: when neither os.UserHomeDir() nor the HOME environment
// variable can be resolved, defaultAuthKeyPath must return "" rather than the
// literal string "~/.ssh/id_rsa" which the SSH library cannot expand.
func TestDefaultAuthKeyPathNoLiteralTilde(t *testing.T) {
// Unset HOME so that os.UserHomeDir() fails and the fallback env var is
// also empty. t.Setenv restores the original value after the test.
t.Setenv("HOME", "")
got := defaultAuthKeyPath()
if got == "~/.ssh/id_rsa" {
t.Fatal("defaultAuthKeyPath returned literal '~/.ssh/id_rsa' when HOME is empty; expected \"\"")
}
if got != "" {
t.Fatalf("defaultAuthKeyPath returned %q when HOME is empty; expected \"\"", got)
}
}
// TestDefaultAuthKeyPathWithHome verifies that when HOME is set, the returned
// path is an absolute path constructed with filepath.Join (no literal '~').
func TestDefaultAuthKeyPathWithHome(t *testing.T) {
t.Setenv("HOME", "/home/testuser")
got := defaultAuthKeyPath()
want := "/home/testuser/.ssh/id_rsa"
if got != want {
t.Fatalf("defaultAuthKeyPath() = %q; want %q", got, want)
}
}
func writeTestConfig(t *testing.T, path, body string) {
t.Helper()
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("write config failed: %v", err)
}
}
|